From 84908a6fdd98724830dacdfc04e2ccd744195872 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:46:50 +0200 Subject: [PATCH 01/91] =?UTF-8?q?=E2=AC=86=20Bump=20the=20python-packages?= =?UTF-8?q?=20group=20with=203=20updates=20(#2398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov --- backend/pyproject.toml | 2 +- pyproject.toml | 4 +-- uv.lock | 74 +++++++++++++++++++----------------------- 3 files changed, 36 insertions(+), 44 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index eec71af8..7663f9aa 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "" requires-python = ">=3.14,<4.0" dependencies = [ - "fastapi[standard]>=0.139.0,<1.0.0", + "fastapi[standard]>=0.141.1,<1.0.0", "python-multipart<1.0.0,>=0.0.27", "email-validator<3.0.0.0,>=2.1.0.post1", "tenacity<10.0.0,>=8.2.3", diff --git a/pyproject.toml b/pyproject.toml index 2c4dfcf8..c7eed2e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [dependency-groups] dev = [ - "prek>=0.4.8,<1.0.0", - "zizmor>=1.25.2", + "prek>=0.4.11,<1.0.0", + "zizmor>=1.28.0", ] github-actions = [ diff --git a/uv.lock b/uv.lock index 230ba1ef..8c40bd42 100644 --- a/uv.lock +++ b/uv.lock @@ -13,8 +13,8 @@ members = [ [manifest.dependency-groups] dev = [ - { name = "prek", specifier = ">=0.4.8,<1.0.0" }, - { name = "zizmor", specifier = ">=1.25.2" }, + { name = "prek", specifier = ">=0.4.11,<1.0.0" }, + { name = "zizmor", specifier = ">=1.28.0" }, ] github-actions = [{ name = "smokeshow", specifier = ">=0.5.0" }] @@ -98,7 +98,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.12.1,<2.0.0" }, { name = "email-validator", specifier = ">=2.1.0.post1,<3.0.0.0" }, { name = "emails", specifier = ">=1.1.2,<2.0" }, - { name = "fastapi", extras = ["standard"], specifier = ">=0.139.0,<1.0.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.141.1,<1.0.0" }, { name = "httpx", specifier = ">=0.25.1,<1.0.0" }, { name = "jinja2", specifier = ">=3.1.4,<4.0.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.1.13,<4.0.0" }, @@ -529,9 +529,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" }, { url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" }, { url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/2664d290cbd1fef9eb3f69b5d3bc5aa91b6fa907519298ca6af93a90c6cb/greenlet-3.5.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", size = 669989, upload-time = "2026-06-17T18:39:30.79Z" }, { url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" }, - { url = "https://files.pythonhosted.org/packages/42/d4/fcb53fa9847d7fbd4723fbed9469c3869b9e3544c4e001d9d5aa2f66162d/greenlet-3.5.2-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", size = 472888, upload-time = "2026-06-17T18:41:22.511Z" }, { url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" }, { url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" }, { url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" }, @@ -539,18 +537,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" }, { url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" }, { url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" }, - { url = "https://files.pythonhosted.org/packages/35/ad/9b3058f999b81750a9c6d9ec424f509462d232b58002086fe2ba63b66407/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", size = 658945, upload-time = "2026-06-17T18:39:32.509Z" }, { url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" }, - { url = "https://files.pythonhosted.org/packages/92/75/1b6ecd8c027b69ab1b6798a84094df79aab5e69ac7e249c78b9d361dd1fa/greenlet-3.5.2-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", size = 490529, upload-time = "2026-06-17T18:41:23.954Z" }, { url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" }, { url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" }, { url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" }, { url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" }, { url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" }, { url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/53/e0/4ce3a046b51e53934eae93d7f9c13975a97285741e9e1fcadf8751314c37/greenlet-3.5.2-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", size = 673494, upload-time = "2026-06-17T18:39:34.196Z" }, { url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" }, - { url = "https://files.pythonhosted.org/packages/52/e0/9c18721e63445dce02ee67e4c81c0f281626604ff55ae6f7b7f4354d7129/greenlet-3.5.2-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", size = 479721, upload-time = "2026-06-17T18:41:25.726Z" }, { url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" }, { url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" }, { url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" }, @@ -558,9 +552,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" }, { url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/87/10776cd88df54d0f563e9e21e98363f2d6af94bedc553b1da0972fa87f80/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", size = 663191, upload-time = "2026-06-17T18:39:35.639Z" }, { url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" }, - { url = "https://files.pythonhosted.org/packages/02/aa/26ddf92826a99d87bfb8fdb8f3a262a6f16495a5d8e579737baa92fb4543/greenlet-3.5.2-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", size = 498199, upload-time = "2026-06-17T18:41:27.464Z" }, { url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" }, { url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" }, { url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" }, @@ -821,26 +813,26 @@ wheels = [ [[package]] name = "prek" -version = "0.4.8" +version = "0.4.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/46/e436a6eb9fdb4d3fd08d0ab7fdba19fe03a9e994ec810de57869b853bd8e/prek-0.4.8.tar.gz", hash = "sha256:d15d8bef72ab7b02c7dc01458ac9e05b3131534492b5ce9bb11c4f6f636fa868", size = 494570, upload-time = "2026-07-04T12:05:10.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1a/73b6dae5ce7e997cb35a69bfe1d25a798e85fa3d2eabf95324563f461b30/prek-0.4.11.tar.gz", hash = "sha256:4a14cb9bbae850605ae3904fbdbb12f0e00c12455efaa2266da8fb8e5c0350d7", size = 516254, upload-time = "2026-07-24T17:05:35.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/78/b4149c8913ced2e42debb49e261c4788a1ce431e84226921c2e1a7ea8545/prek-0.4.8-py3-none-linux_armv6l.whl", hash = "sha256:1f8f8cdc65836b571824c965daebb81b449f7e4a43894c58621f5708d5a185ed", size = 5668955, upload-time = "2026-07-04T12:04:41.588Z" }, - { url = "https://files.pythonhosted.org/packages/76/5f/7f54a0087b6b2f1751aeb41266d9c15e66fd0055492814798ab818cd0414/prek-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bce1798e96d9e3a6e6abf435da7107e81452f69edb3ca7c6f90a457355ea46e2", size = 6030947, upload-time = "2026-07-04T12:04:43.8Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/f2829fc3902920c36b764a386fa303e71a8219dac25cb3827c575e84199a/prek-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab3a52db17254d701c3cebb7eea58c8230aa7c1959aacfd5b5f25de18edb15d1", size = 5572593, upload-time = "2026-07-04T12:04:45.763Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/c5589955bcd5e3e33b67d8bc3110818cecac82a38fd6bc8b5dfdc5de421c/prek-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b3fcfd620523bbc3f51a21d7cd63449f659b9e2cf3582de12dd5949e23227b8f", size = 5847150, upload-time = "2026-07-04T12:04:47.419Z" }, - { url = "https://files.pythonhosted.org/packages/2d/9d/1f2dc91bdb79d2c4714b27eac9477a51490fba5b4731330dbbebc76bd345/prek-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42e65bc8425e9d7f1691a13ca1da2e07807d1ba76c35740833354b945131689e", size = 5573738, upload-time = "2026-07-04T12:04:49.125Z" }, - { url = "https://files.pythonhosted.org/packages/81/29/69a7b58e16ecbc5f3989bf4b028018d11a82dcdd320b93d6588d72f32aa7/prek-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f578492a8e0c9bc6b4bf6dfbba8716f647d4cd0769bf10ad6cf336e3096fd392", size = 5981054, upload-time = "2026-07-04T12:04:50.842Z" }, - { url = "https://files.pythonhosted.org/packages/63/cc/9b9850a60c22ed18c7755ebd2d72c6eefb37fac58149d09f6adc4691c2cf/prek-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4335f9d5beb123a3884a7fe34f57c9f0828f4fbb7666beab4298833459b104f", size = 6751350, upload-time = "2026-07-04T12:04:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/01/e5/c425aa7272b430630119e6757def3a2007555ba8cbeb2630e0448e7a8b7f/prek-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a8747df9c602e052881d3efb14dd7f7d62a59bd7277ae5171c9e7661d59d84", size = 6243881, upload-time = "2026-07-04T12:04:54.703Z" }, - { url = "https://files.pythonhosted.org/packages/1c/da/accd3ad07fd2891d3c2777eb42435439fdf11982c51d60f087c0b6b6e102/prek-0.4.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4db639db481d5f854eff9b3d2108889e613b8c15868bcf6bdd777c7cee577436", size = 5848846, upload-time = "2026-07-04T12:04:56.402Z" }, - { url = "https://files.pythonhosted.org/packages/15/00/3477704635249f21f5f98ce444cd7690c2aa9dc8d146a045db88ef2cd8c5/prek-0.4.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c3890a6f92316d2cf44eb50584e8d2b23a596dd70487022e61186a71a2ac0900", size = 5713942, upload-time = "2026-07-04T12:04:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e6/3ca4fabaebeadc976d9a92d1d9130674265355ea3b728418bad61583b097/prek-0.4.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fc7e15c24c591a37c6ffce5b25a021b16c299ac2649f183d812b67d665cd6551", size = 5554725, upload-time = "2026-07-04T12:04:59.96Z" }, - { url = "https://files.pythonhosted.org/packages/a5/46/2ab6aaaeff0cedb8955b2e4032071c8712382bdd423bb849718c3720180d/prek-0.4.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:36fe721704ff0c7624c1167639e23a5fe658bfd38c314f487219c9afd1eeb733", size = 5838595, upload-time = "2026-07-04T12:05:01.861Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8b/91398f2b6cd1629d5d8ca8c85b08eca500814a374313b0193f4aaf6ab6c4/prek-0.4.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:162e544abc394a8124f3a4ad68efee116bad09440e679dbd1675177335c2a432", size = 6357222, upload-time = "2026-07-04T12:05:03.845Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/ce5cbfaad36866134a21754640a05ecdba641fcd7ad15aa74cf3443f34f6/prek-0.4.8-py3-none-win32.whl", hash = "sha256:2602e46c8c5da7dfa69f60fcf88c2b57132ac623f49fb08bfb3094298c5f07e3", size = 5354388, upload-time = "2026-07-04T12:05:05.587Z" }, - { url = "https://files.pythonhosted.org/packages/df/03/3bc908bc5f7e430315553e47dfa055f19923a3888f9afe4da19f244b5cbf/prek-0.4.8-py3-none-win_amd64.whl", hash = "sha256:7cb22da60bee41b89c4978c0bea7126a3c0ccc003dae6748cf29b53947815edc", size = 5748221, upload-time = "2026-07-04T12:05:07.559Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/4295e6d5f5028171dfeb115ad38ab76bf3fe0c8df91b70d73c79aa760a94/prek-0.4.8-py3-none-win_arm64.whl", hash = "sha256:da70057f577b15d4bd121bf9dd29ee205fd4b4d75a0cafba062e84d7e8b4378b", size = 5574425, upload-time = "2026-07-04T12:05:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/a00b2de492a80e99b1698e72c1d196ac3ed544dc7ee0ada261ac066e78e0/prek-0.4.11-py3-none-linux_armv6l.whl", hash = "sha256:3830cb7cc47e837888b8b464ecb21355a69235cfacef0fd89101e17f09345d63", size = 5770511, upload-time = "2026-07-24T17:05:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1e/f97c74defcd5d5645888cb99fcdd9b8b48cc7247cf31e64414d106d56d66/prek-0.4.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45facadf9c2332b28e6ab2744312ae0275a93ab5a437da8bcc53a8c7260cb4b0", size = 6118049, upload-time = "2026-07-24T17:05:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/d9/92/8367d26421ee6fe6019a63928fb0ed31179cd0d6199879c524f89ef4c95c/prek-0.4.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f8a194b1d00d24dff8baff691c99e2668339da2da3482b4ee99c1a0f2409378", size = 5601478, upload-time = "2026-07-24T17:05:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/7617c9b87afaadede4167720aae7ef12d7e51167db903bc8fbac0cadef7b/prek-0.4.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:21d93e6d76bf3d7a9bb70c6ac86ef372adaee50068c45749e6b9e1c2ac4ec939", size = 5932071, upload-time = "2026-07-24T17:05:17.217Z" }, + { url = "https://files.pythonhosted.org/packages/ef/41/6796a4011b04212333259064aa885a71d03d9581ba9bff52db3ce58d1f06/prek-0.4.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7fb07cde2d2156efa6980122b3f13dc88f20c84b933c6205675d0f1e7be2cde8", size = 5677617, upload-time = "2026-07-24T17:05:18.658Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/3e339901f8460b6073313619b4fd9bf4135e48430695c53640b83ace88ea/prek-0.4.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f5e446d2aca739bd18b380578e9c78bb4c086299abc3e167d51c47840c56f", size = 6106370, upload-time = "2026-07-24T17:05:20.219Z" }, + { url = "https://files.pythonhosted.org/packages/8e/4e/94e24b5c1910ec15692ecaf33d0d8ef0d02a02a8b5e40d3c976396880cba/prek-0.4.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7059d640e595d098600e2d97af961f46292b4112670edbe632de84f6828389e", size = 6884342, upload-time = "2026-07-24T17:05:21.587Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/fc0daa033dcafe74990c00af2da1e16922790b9c1b8da7e8eebf1123838b/prek-0.4.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a4df33998fcac878bce3b2c624e1caeb9609f3d562c640702c1234ed815daf", size = 6331365, upload-time = "2026-07-24T17:05:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/d7/36/49f152b8f539930e9685cff0509695ce4054abcecc22f4c16c4e1e5c23d0/prek-0.4.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:866991f387527c5f880ce2cc3ddea9b33cc5b986881f8c7f92524cf0969c1350", size = 5939075, upload-time = "2026-07-24T17:05:24.249Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fe/7b097af9161edae7ddedcc9f5cda4a5f31346a492ce3f92583d96c46f628/prek-0.4.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:247e5d8740e137ebdf24fa96f01a95b2d2f1892e956a1ab00c7b1474a59ebcc0", size = 5799029, upload-time = "2026-07-24T17:05:25.652Z" }, + { url = "https://files.pythonhosted.org/packages/49/63/dc955ff99e1002d3cd375b21467c87046bc41deddfce86d898240757b151/prek-0.4.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:603ba9f2fd9d666dddb3ae190a25a5c55091b843cde90ed52e0a1116f50d4062", size = 5651211, upload-time = "2026-07-24T17:05:27.027Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/bf2139ec25eefb5afef43afac7620c5181f2c2661f220598beacb1771207/prek-0.4.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f2682ece3c5fc7201106c4fdd84b0587ccea4b8a2ecefa3e94d3074d2841f1df", size = 5954784, upload-time = "2026-07-24T17:05:28.391Z" }, + { url = "https://files.pythonhosted.org/packages/fc/29/3fe5990aee1bd7c4d50a03358ad867c5e912d9510aafb83a359c7347e5fa/prek-0.4.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:22721d30394192931fecc80d6fc6dd47e8ddf8db8b9693805aba8ec0f50087ec", size = 6448916, upload-time = "2026-07-24T17:05:29.837Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fb/abddacf43738302242ecd237236c7c80cd7c1d27545e16e803770aee76e9/prek-0.4.11-py3-none-win32.whl", hash = "sha256:8b093e7624522146049e994d5cf283d01d71632b638620b79ae0f96afeaa2624", size = 5483539, upload-time = "2026-07-24T17:05:31.228Z" }, + { url = "https://files.pythonhosted.org/packages/00/1e/c293f7a15cb93963c4be36a02e144b93f43906bc02644a3f04e5708e7453/prek-0.4.11-py3-none-win_amd64.whl", hash = "sha256:5a3d7c80b970b456e5f1bcec8382008ee1ae6a3f324a6b9bb4ff7e666ab0f3c4", size = 5861119, upload-time = "2026-07-24T17:05:32.479Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0c/05fe6eb9d6a54d0e02dfa8cc5ad6f86869bf953419ec15909a32466a28ee/prek-0.4.11-py3-none-win_arm64.whl", hash = "sha256:e7b0df37ce05e45a14a9da39ab104691474d72f139bf4f6c860f754763a322cb", size = 5626386, upload-time = "2026-07-24T17:05:33.813Z" }, ] [[package]] @@ -1491,18 +1483,18 @@ wheels = [ [[package]] name = "zizmor" -version = "1.26.1" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/a0/a29b38e24981b4bb41db4f292b2c9fb9ddf8b05d6b724abddd7bd108b621/zizmor-1.26.1.tar.gz", hash = "sha256:0c2cc575007a4db99d89d5acc6120cfa7b61504bc2394c3b50af348c73f1916e", size = 535275, upload-time = "2026-06-21T02:47:21.265Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/92/6b58872ac1184a441037bdc05c658a3900f49401b25f6e834a268ef6ff52/zizmor-1.28.0.tar.gz", hash = "sha256:6d5a300b80bf4c12e9cbe78ffd3874ec3f94b65bea78c994ec18157e2846ece0", size = 550169, upload-time = "2026-07-21T22:16:27.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/a9/2f47f7db8db9491025e00a7f1a0f25d32b642c0285b2fe070ac63e679b47/zizmor-1.26.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7ea21ca959c8e888de238fee81d73a1fdf89a82067eac75b8f1acdbd23e2eeaf", size = 9086061, upload-time = "2026-06-21T02:46:57.492Z" }, - { url = "https://files.pythonhosted.org/packages/58/92/cf6801f01e1d65cbda89a2e2926ea42caf1daad9ffa3f1fc88e4c68f48a9/zizmor-1.26.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:78083b495593f8b0b9dec14036a0836a5afcddda8a40738336ff4e399476b741", size = 8626865, upload-time = "2026-06-21T02:47:00.323Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b1/ff38fc2921f1fb13244bb3a3642c4b45ecf3946c279942aafcb5dbf55a57/zizmor-1.26.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:bb7ebbe565a3742eb49a590352127ad549bb122b9b4ff9424ebab7525fa3b6b6", size = 8843965, upload-time = "2026-06-21T02:47:03.318Z" }, - { url = "https://files.pythonhosted.org/packages/3d/06/c07fd0eeef0427d93e99d552d5386526fbcd0bf05fc95cd37bdc6229fccb/zizmor-1.26.1-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:d3049010b6bd6f849413b6d20c28e0c677b90e0a5b2bc73cbee7f7bd86dc5828", size = 8386985, upload-time = "2026-06-21T02:47:05.741Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2b/61ab13d45d6ce57ef5a08bb3246981f62e30bb4938098b17bb7b88110b79/zizmor-1.26.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6a958d8a0941d7e1d0de8436670b5cb7fc64c8028b4d16e3f519ccc77f953cef", size = 9257232, upload-time = "2026-06-21T02:47:07.941Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ad/bd74a96cb02045414ec5b573cd97ff3b82a97fd0bd6658f93c36a011c439/zizmor-1.26.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d2744cdf944436ca7a009ae8b626a017a40381ec990216abd6cf6b8beb23323a", size = 8873798, upload-time = "2026-06-21T02:47:10.472Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7e/fb3d608ee11e2f619d43ad93bab46eb7b32769fa82b1d86fd23f27c2585b/zizmor-1.26.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:44099f426af9da750ff9f548a0084e11d7d83e0158fe1a2778672398d728efdd", size = 8350857, upload-time = "2026-06-21T02:47:12.748Z" }, - { url = "https://files.pythonhosted.org/packages/27/cc/82d7a838c2d490071555c364f90eb851044b3eeefc1d68612179a2cd1ae5/zizmor-1.26.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8313cc264dec792f00a7328eb7c8e89e7d62d54f950fc897d1e6a5a6e5762203", size = 9351148, upload-time = "2026-06-21T02:47:14.777Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ee/d2a2301f30b9e1bf0d721bfd31739acd71e048757f9ba79279583eb30ac0/zizmor-1.26.1-py3-none-win32.whl", hash = "sha256:c96d7787d69fb298eae939e00dfdf7f534d7dfbd9cc17ab442c0650a56851415", size = 7531021, upload-time = "2026-06-21T02:47:17.247Z" }, - { url = "https://files.pythonhosted.org/packages/91/58/ad561f3a5057d3c0f152002e180a3a5745e72ea9d69bf66450ef9f5d3fe5/zizmor-1.26.1-py3-none-win_amd64.whl", hash = "sha256:0a05acf6068609fb6df3b137276cf18a686226a1e0e207941cb34a85929f16cf", size = 8616584, upload-time = "2026-06-21T02:47:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/e8/48/a725a1c34e5565eeeb4e4a93c59662f206e5156f2029627c66b2373edf55/zizmor-1.28.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:149dba59a8bd2897960ee54c40ae8b5801a71293bad71c8d2913c74ab66a294c", size = 8909502, upload-time = "2026-07-21T22:16:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/ba6d0eb2b336b4c4826bb0c9ed23c3239d46d71d8c1ddd2d395efeff7f52/zizmor-1.28.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0949f57a6d20deeb9c509705afce8de233c166475e25be305985a1fe553c6e0d", size = 8520554, upload-time = "2026-07-21T22:16:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4f/07/69aba8157056fc0949cbdc787d8437813961e204a117c629d34afc827d6d/zizmor-1.28.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:9819f91f0ef486e4af98a6aacfed23c6b4067fabcecb88d49231228a3d43f821", size = 8759226, upload-time = "2026-07-21T22:16:11.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/b65717a3ca4573611f47612e67be5458ff700e857f49f759741fa67b0519/zizmor-1.28.0-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:354e6cb98a15a88593a6f7ac6236b092b83a2aad5c4768ee750f9b3262f668d9", size = 8382005, upload-time = "2026-07-21T22:16:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b4/f823bd2a1ba6dc432fdcbd249d4c442ce79bd137d34d986fce5c181f2800/zizmor-1.28.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:ae2cab67ce713e760e0d1b61ad749d374693ea2b310337aab11cd446748267f3", size = 9175601, upload-time = "2026-07-21T22:16:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/adad17b0320eb3bc9ed797019fccf69a809affdf5ba9402bdc8b910957e1/zizmor-1.28.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7b00018cf2cc948c3b3e010c1a1f30fdc001f0d062640469f17e0ef006e74eb7", size = 8792861, upload-time = "2026-07-21T22:16:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3d/4d5583f24730f404c104eacf8aca4ca5a3d4c480e9d3cfe3eadd93351fd6/zizmor-1.28.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6897f02b0d02fd709f5ebc13cd37d97ad464219e0ba86a03f6d86f7777b7b102", size = 8343070, upload-time = "2026-07-21T22:16:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/97/e2/4521e6dd56bc0d44eefb177794e261925c5cd00ab647b17e5513bddcb985/zizmor-1.28.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ef69d198dcf6835c9b6eea8673cbc5c36f10b3f1b98b51d28266b7e01c3704d4", size = 9262222, upload-time = "2026-07-21T22:16:21.89Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/5d841d197c821921cb8243352c610ca2505a86871c10f81efe4da190d24c/zizmor-1.28.0-py3-none-win32.whl", hash = "sha256:833c360ba5a9c74ca45007b8c79939826fca0c5ed65144fa08f63a7009cef096", size = 7541161, upload-time = "2026-07-21T22:16:23.965Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f5/3f4591746e5a9c7e8efc2e9e02aa43e1aa0957fe33cae64ce386c74b882e/zizmor-1.28.0-py3-none-win_amd64.whl", hash = "sha256:c93b30d211b0b0c38905a8803cc2653fff37de03eb77105302a02f709773bd4a", size = 8619340, upload-time = "2026-07-21T22:16:25.6Z" }, ] From 24db0e9e186a2607d4005c43bbc7dcce56eaf860 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Aug 2026 19:47:21 +0000 Subject: [PATCH 02/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index f0b82ea1..d9bbc578 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* ⬆ Bump the python-packages group with 3 updates. PR [#2398](https://github.com/fastapi/full-stack-fastapi-template/pull/2398) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Improve workflows for private repos. PR [#2394](https://github.com/fastapi/full-stack-fastapi-template/pull/2394) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade latest-changes to 0.7.1. PR [#2389](https://github.com/fastapi/full-stack-fastapi-template/pull/2389) by [@tiangolo](https://github.com/tiangolo). * Bump axios from 1.16.0 to 1.18.0 in /frontend. PR [#2386](https://github.com/fastapi/full-stack-fastapi-template/pull/2386) by [@dependabot[bot]](https://github.com/apps/dependabot). From af27db9a1f7edfa2666c5fe2b93d6131466c6ccd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:47:38 +0200 Subject: [PATCH 03/91] =?UTF-8?q?=E2=AC=86=20Update=20sentry-sdk[fastapi]?= =?UTF-8?q?=20requirement=20from=20<3.0.0,>=3D2.63.0=20to=20>=3D2.66.1,<3.?= =?UTF-8?q?0.0=20(#2400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 7663f9aa..82b14979 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "psycopg[binary]<4.0.0,>=3.1.13", "sqlmodel>=0.0.39,<1.0.0", "pydantic-settings<3.0.0,>=2.2.1", - "sentry-sdk[fastapi]>=2.63.0,<3.0.0", + "sentry-sdk[fastapi]>=2.66.1,<3.0.0", "pyjwt<3.0.0,>=2.13.0", "pwdlib[argon2,bcrypt]>=0.3.0", ] diff --git a/uv.lock b/uv.lock index 8c40bd42..40134b06 100644 --- a/uv.lock +++ b/uv.lock @@ -107,7 +107,7 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.2.1,<3.0.0" }, { name = "pyjwt", specifier = ">=2.13.0,<3.0.0" }, { name = "python-multipart", specifier = ">=0.0.27,<1.0.0" }, - { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.63.0,<3.0.0" }, + { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.66.1,<3.0.0" }, { name = "sqlmodel", specifier = ">=0.0.39,<1.0.0" }, { name = "tenacity", specifier = ">=8.2.3,<10.0.0" }, ] @@ -1175,15 +1175,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.63.0" +version = "2.66.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/c8/b3c970a5b186722d276cd40a05b3254e03bccc0208560aff20f612e018e8/sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216", size = 912449, upload-time = "2026-06-16T12:45:57.553Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/57/cb205f7d93373120f666b9c5736dc0815524d96a9b278e7a728f018dc22a/sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a", size = 495950, upload-time = "2026-06-16T12:45:55.819Z" }, + { url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" }, ] [package.optional-dependencies] From 24ff71fdac77d0d344d24d7a50f2a8a08151ff30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Aug 2026 19:48:08 +0000 Subject: [PATCH 04/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index d9bbc578..c0102c8b 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.63.0 to >=2.66.1,<3.0.0. PR [#2400](https://github.com/fastapi/full-stack-fastapi-template/pull/2400) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the python-packages group with 3 updates. PR [#2398](https://github.com/fastapi/full-stack-fastapi-template/pull/2398) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Improve workflows for private repos. PR [#2394](https://github.com/fastapi/full-stack-fastapi-template/pull/2394) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade latest-changes to 0.7.1. PR [#2389](https://github.com/fastapi/full-stack-fastapi-template/pull/2389) by [@tiangolo](https://github.com/tiangolo). From 752fc77beda45f0953fbddcfa28feff22e94bac6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:51:06 +0200 Subject: [PATCH 05/91] =?UTF-8?q?=E2=AC=86=20Bump=20the=20github-actions?= =?UTF-8?q?=20group=20with=205=20updates=20(#2397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bump-pre-commit-hooks.yml | 6 +++--- .github/workflows/deploy-production.yml | 2 +- .github/workflows/deploy-staging.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/playwright.yml | 10 +++++----- .github/workflows/pre-commit.yml | 8 ++++---- .github/workflows/smokeshow.yml | 6 +++--- .github/workflows/test-backend.yml | 6 +++--- .github/workflows/test-docker-compose.yml | 2 +- .github/workflows/zizmor.yml | 4 ++-- 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/bump-pre-commit-hooks.yml b/.github/workflows/bump-pre-commit-hooks.yml index f1a9fe40..9442b353 100644 --- a/.github/workflows/bump-pre-commit-hooks.yml +++ b/.github/workflows/bump-pre-commit-hooks.yml @@ -17,16 +17,16 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ secrets.LATEST_CHANGES }} persist-credentials: true - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: ".python-version" - name: Setup uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index f09ccd77..f86c79e6 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -32,7 +32,7 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_DSN }} steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 2f75ecd3..21adf429 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -32,7 +32,7 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_DSN }} steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 539727d1..f6a91dc7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 + - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} - run: echo "Done adding labels" # Run this after labeler applied labels diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 4b37eeb2..45a3246e 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -30,7 +30,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.repository.default_branch }} # To allow latest-changes to commit to the default branch diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 44ea94f5..4c4f9019 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -23,7 +23,7 @@ jobs: outputs: changed: ${{ steps.filter.outputs.changed }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 persist-credentials: false @@ -51,13 +51,13 @@ jobs: shardTotal: [4] fail-fast: false steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.12 - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: .python-version - name: Setup tmate session @@ -66,7 +66,7 @@ jobs: with: limit-access-to-actor: true - name: Install uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 @@ -99,7 +99,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 244d0144..98c50d66 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -18,7 +18,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout PR for own repo if: env.HAS_SECRETS == 'true' with: @@ -31,7 +31,7 @@ jobs: token: ${{ secrets.PRE_COMMIT }} # zizmor: ignore[secrets-outside-env] persist-credentials: true # Required for `git push` command # pre-commit lite ci needs the default checkout configs to work - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout PR for fork if: env.HAS_SECRETS == 'false' with: @@ -43,11 +43,11 @@ jobs: with: bun-version: 1.3.12 - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: .python-version - name: Setup uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 3932401d..1483e5b3 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -17,14 +17,14 @@ jobs: statuses: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: .python-version - name: Setup uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index f96bc03c..93c169a9 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -14,15 +14,15 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: .python-version - name: Install uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 diff --git a/.github/workflows/test-docker-compose.yml b/.github/workflows/test-docker-compose.yml index 4d7c27de..c36ea63f 100644 --- a/.github/workflows/test-docker-compose.yml +++ b/.github/workflows/test-docker-compose.yml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - run: docker compose build diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index e68fe88c..fe96037f 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -20,11 +20,11 @@ jobs: security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 + uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 with: advanced-security: ${{ github.repository_owner == 'fastapi' }} annotations: ${{ github.repository_owner != 'fastapi' }} From 97f28a5905219247cef461cfde3c43b802c810a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Aug 2026 19:52:10 +0000 Subject: [PATCH 06/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index c0102c8b..6d1b55c7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* ⬆ Bump the github-actions group with 5 updates. PR [#2397](https://github.com/fastapi/full-stack-fastapi-template/pull/2397) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.63.0 to >=2.66.1,<3.0.0. PR [#2400](https://github.com/fastapi/full-stack-fastapi-template/pull/2400) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the python-packages group with 3 updates. PR [#2398](https://github.com/fastapi/full-stack-fastapi-template/pull/2398) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Improve workflows for private repos. PR [#2394](https://github.com/fastapi/full-stack-fastapi-template/pull/2394) by [@tiangolo](https://github.com/tiangolo). From 1b4d46c0b313569410484d131a76adcd576c0c94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:55:26 +0000 Subject: [PATCH 07/91] =?UTF-8?q?=E2=AC=86=20Update=20psycopg[binary]=20re?= =?UTF-8?q?quirement=20from=20<4.0.0,>=3D3.1.13=20to=20>=3D3.3.4,<4.0.0=20?= =?UTF-8?q?(#2399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 82b14979..c473adac 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "jinja2<4.0.0,>=3.1.4", "alembic<2.0.0,>=1.12.1", "httpx<1.0.0,>=0.25.1", - "psycopg[binary]<4.0.0,>=3.1.13", + "psycopg[binary]>=3.3.4,<4.0.0", "sqlmodel>=0.0.39,<1.0.0", "pydantic-settings<3.0.0,>=2.2.1", "sentry-sdk[fastapi]>=2.66.1,<3.0.0", diff --git a/uv.lock b/uv.lock index 40134b06..8ce206e1 100644 --- a/uv.lock +++ b/uv.lock @@ -101,7 +101,7 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.141.1,<1.0.0" }, { name = "httpx", specifier = ">=0.25.1,<1.0.0" }, { name = "jinja2", specifier = ">=3.1.4,<4.0.0" }, - { name = "psycopg", extras = ["binary"], specifier = ">=3.1.13,<4.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.3.4,<4.0.0" }, { name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" }, { name = "pydantic", specifier = ">2.0" }, { name = "pydantic-settings", specifier = ">=2.2.1,<3.0.0" }, From 750d3d0bc6dfece4dec2d6ef8c3ff7e64f72545d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Aug 2026 19:55:53 +0000 Subject: [PATCH 08/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 6d1b55c7..32f6be7e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.1.13 to >=3.3.4,<4.0.0. PR [#2399](https://github.com/fastapi/full-stack-fastapi-template/pull/2399) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the github-actions group with 5 updates. PR [#2397](https://github.com/fastapi/full-stack-fastapi-template/pull/2397) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.63.0 to >=2.66.1,<3.0.0. PR [#2400](https://github.com/fastapi/full-stack-fastapi-template/pull/2400) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the python-packages group with 3 updates. PR [#2398](https://github.com/fastapi/full-stack-fastapi-template/pull/2398) by [@dependabot[bot]](https://github.com/apps/dependabot). From de92a0d9fae968ab6a76fee38ce56dff3333deed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 6 Aug 2026 15:10:23 +0200 Subject: [PATCH 09/91] =?UTF-8?q?=F0=9F=94=A5=20Remove=20the=20old=20Lates?= =?UTF-8?q?t=20Changes=20workflow=20(#2403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 42 ---------------------------- 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/latest-changes.yml diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml deleted file mode 100644 index 45a3246e..00000000 --- a/.github/workflows/latest-changes.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Latest Changes - -on: - pull_request_target: # zizmor: ignore[dangerous-triggers] - branches: - - master - types: - - closed - workflow_dispatch: - inputs: - number: - description: PR number - required: true - debug_enabled: - description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" - required: false - default: "false" - -permissions: {} - -jobs: - latest-changes: - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - pull-requests: read - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.event.repository.default_branch }} - # To allow latest-changes to commit to the default branch - token: ${{ secrets.LATEST_CHANGES }} # zizmor: ignore[secrets-outside-env] - persist-credentials: true # required by tiangolo/latest-changes - - uses: tiangolo/latest-changes@8a940392f4c65274539453a5d5a76d9550203ac1 # 0.7.1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - debug_logs: true From f8b7926d1cec1e81ea9b99b3c498022071ca85cd Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:10:26 +0000 Subject: [PATCH 10/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 32f6be7e..aed3b375 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.1.13 to >=3.3.4,<4.0.0. PR [#2399](https://github.com/fastapi/full-stack-fastapi-template/pull/2399) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the github-actions group with 5 updates. PR [#2397](https://github.com/fastapi/full-stack-fastapi-template/pull/2397) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.63.0 to >=2.66.1,<3.0.0. PR [#2400](https://github.com/fastapi/full-stack-fastapi-template/pull/2400) by [@dependabot[bot]](https://github.com/apps/dependabot). From a8f1ba0e70e96414fa020cd3bede7daac73fd753 Mon Sep 17 00:00:00 2001 From: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:25:28 +0200 Subject: [PATCH 11/91] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20`@hey-api/ope?= =?UTF-8?q?napi-ts`=20from=200.73.0=20to=200.97.3=20and=20migrate=20to=20n?= =?UTF-8?q?ew=20client=20(#2388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bun.lock | 124 +-- frontend/openapi-ts.config.ts | 28 +- frontend/package.json | 2 +- frontend/src/client/client.gen.ts | 16 + frontend/src/client/client/client.gen.ts | 161 +++ frontend/src/client/client/index.ts | 23 + frontend/src/client/client/types.gen.ts | 169 ++++ frontend/src/client/client/utils.gen.ts | 206 ++++ frontend/src/client/core/ApiError.ts | 21 - frontend/src/client/core/ApiRequestOptions.ts | 21 - frontend/src/client/core/ApiResult.ts | 7 - frontend/src/client/core/CancelablePromise.ts | 126 --- frontend/src/client/core/OpenAPI.ts | 57 -- frontend/src/client/core/auth.gen.ts | 41 + .../src/client/core/bodySerializer.gen.ts | 82 ++ frontend/src/client/core/params.gen.ts | 169 ++++ .../src/client/core/pathSerializer.gen.ts | 171 ++++ .../src/client/core/queryKeySerializer.gen.ts | 117 +++ frontend/src/client/core/request.ts | 347 ------- .../src/client/core/serverSentEvents.gen.ts | 242 +++++ frontend/src/client/core/types.gen.ts | 104 ++ frontend/src/client/core/utils.gen.ts | 140 +++ frontend/src/client/index.ts | 8 +- frontend/src/client/schemas.gen.ts | 571 ----------- frontend/src/client/sdk.gen.ts | 592 +++++------ frontend/src/client/types.gen.ts | 923 ++++++++++++++++-- frontend/src/components/Admin/AddUser.tsx | 3 +- frontend/src/components/Admin/DeleteUser.tsx | 2 +- frontend/src/components/Admin/EditUser.tsx | 2 +- frontend/src/components/Items/AddItem.tsx | 3 +- frontend/src/components/Items/DeleteItem.tsx | 2 +- frontend/src/components/Items/EditItem.tsx | 2 +- .../UserSettings/ChangePassword.tsx | 2 +- .../UserSettings/UserInformation.tsx | 2 +- frontend/src/hooks/useAuth.ts | 8 +- frontend/src/main.tsx | 16 +- frontend/src/routes/_layout/admin.tsx | 5 +- frontend/src/routes/_layout/items.tsx | 3 +- frontend/src/routes/recover-password.tsx | 2 +- frontend/src/routes/reset-password.tsx | 2 +- frontend/src/utils.ts | 22 +- frontend/tests/utils/privateApi.ts | 12 +- 42 files changed, 2820 insertions(+), 1736 deletions(-) create mode 100644 frontend/src/client/client.gen.ts create mode 100644 frontend/src/client/client/client.gen.ts create mode 100644 frontend/src/client/client/index.ts create mode 100644 frontend/src/client/client/types.gen.ts create mode 100644 frontend/src/client/client/utils.gen.ts delete mode 100644 frontend/src/client/core/ApiError.ts delete mode 100644 frontend/src/client/core/ApiRequestOptions.ts delete mode 100644 frontend/src/client/core/ApiResult.ts delete mode 100644 frontend/src/client/core/CancelablePromise.ts delete mode 100644 frontend/src/client/core/OpenAPI.ts create mode 100644 frontend/src/client/core/auth.gen.ts create mode 100644 frontend/src/client/core/bodySerializer.gen.ts create mode 100644 frontend/src/client/core/params.gen.ts create mode 100644 frontend/src/client/core/pathSerializer.gen.ts create mode 100644 frontend/src/client/core/queryKeySerializer.gen.ts delete mode 100644 frontend/src/client/core/request.ts create mode 100644 frontend/src/client/core/serverSentEvents.gen.ts create mode 100644 frontend/src/client/core/types.gen.ts create mode 100644 frontend/src/client/core/utils.gen.ts delete mode 100644 frontend/src/client/schemas.gen.ts diff --git a/bun.lock b/bun.lock index 42f5d531..76c17b6b 100644 --- a/bun.lock +++ b/bun.lock @@ -46,7 +46,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.16", - "@hey-api/openapi-ts": "0.73.0", + "@hey-api/openapi-ts": "0.97.3", "@playwright/test": "1.61.1", "@tanstack/router-devtools": "^1.167.0", "@tanstack/router-plugin": "^1.168.18", @@ -178,9 +178,17 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.0.6", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w=="], + "@hey-api/codegen-core": ["@hey-api/codegen-core@0.8.2", "", { "dependencies": { "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", "c12": "3.3.4", "color-support": "1.1.3" } }, "sha512-R2NMf3wq97rh1mjz33WJQU8svz3F0RYUjvx/QzXucjpSqQ3O5huTdDjErG4fMxSr1X+X56NuDrqtGfHmo1TRUQ=="], - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.73.0", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "open": "10.1.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-sUscR3OIGW0k9U//28Cu6BTp3XaogWMDORj9H+5Du9E5AvTT7LZbCEDvkLhebFOPkp2cZAQfd66HiZsiwssBcQ=="], + "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.4.2", "", { "dependencies": { "@jsdevtools/ono": "7.1.3", "@types/json-schema": "7.0.15", "js-yaml": "4.1.1" } }, "sha512-ZhCFSKI2ipZHEbgmtUHdyddvRU3wJ4elgCfYUC7T7hZa4EivSrVflTQf2w+v3TuaYxR1Y2V2kq3otqTttrrK8Q=="], + + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.97.3", "", { "dependencies": { "@hey-api/codegen-core": "0.8.2", "@hey-api/json-schema-ref-parser": "1.4.2", "@hey-api/shared": "0.4.5", "@hey-api/spec-types": "0.2.0", "@hey-api/types": "0.1.4", "@lukeed/ms": "2.0.2", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.3", "get-tsconfig": "4.14.0" }, "peerDependencies": { "typescript": ">=5.5.3 || >=6.0.0 || 6.0.1-rc" }, "bin": { "openapi-ts": "./bin/run.js" } }, "sha512-4sR6/E/POuy7aPZW9DDjhObzZCq7eSJWiW0+epXeKNczoTWEwdOyWFy9Ca/CnXYlZ3oJsrv0ZD0OO+YuczT7CA=="], + + "@hey-api/shared": ["@hey-api/shared@0.4.5", "", { "dependencies": { "@hey-api/codegen-core": "0.8.2", "@hey-api/json-schema-ref-parser": "1.4.2", "@hey-api/spec-types": "0.2.0", "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", "semver": "7.7.4" } }, "sha512-au4eHpBXAe1du0iMp6ESYuEaMS2jsoEyrbcT246btRhI9rMeQFEs7ZjtcMGXGsxhpaR38A8cPGNHx7QOrWAdMw=="], + + "@hey-api/spec-types": ["@hey-api/spec-types@0.2.0", "", { "dependencies": { "@hey-api/types": "0.1.4" } }, "sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg=="], + + "@hey-api/types": ["@hey-api/types@0.1.4", "", {}, "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg=="], "@hookform/resolvers": ["@hookform/resolvers@5.4.0", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw=="], @@ -196,6 +204,8 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], @@ -420,8 +430,6 @@ "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.3.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w=="], - "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -444,7 +452,7 @@ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - "c12": ["c12@2.0.1", "", { "dependencies": { "chokidar": "^4.0.1", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^2.3.0", "mlly": "^1.7.1", "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A=="], + "c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -452,10 +460,6 @@ "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], - - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -464,16 +468,16 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@13.0.0", "", {}, "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -484,7 +488,7 @@ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], @@ -516,6 +520,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "exsolve": ["exsolve@1.1.0", "", {}, "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], @@ -524,8 +530,6 @@ "frontend": ["frontend@workspace:frontend"], - "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -538,9 +542,9 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": "dist/cli.mjs" }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], + "giget": ["giget@3.3.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw=="], "goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="], @@ -548,8 +552,6 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": "bin/handlebars" }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -560,12 +562,16 @@ "is-docker": ["is-docker@3.0.0", "", { "bin": "cli.js" }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": "cli.js" }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "isbot": ["isbot@5.1.33", "", {}, "sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -600,8 +606,6 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], @@ -614,43 +618,29 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], - - "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], - - "mkdirp": ["mkdirp@1.0.4", "", { "bin": "bin/cmd.js" }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - - "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": "dist/cli.mjs" }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], - "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="], @@ -658,11 +648,13 @@ "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "prettier": ["prettier@3.8.0", "", { "bin": "bin/prettier.cjs" }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="], "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], @@ -690,15 +682,17 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], - "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -708,10 +702,6 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -722,10 +712,6 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - - "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], @@ -742,12 +728,18 @@ "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], @@ -762,8 +754,6 @@ "@tanstack/router-generator/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "@tanstack/router-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -772,24 +762,12 @@ "es-set-tostringtag/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "get-intrinsic/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "tsx/get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "unplugin/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "@tanstack/router-utils/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], diff --git a/frontend/openapi-ts.config.ts b/frontend/openapi-ts.config.ts index b5a69e20..3435ef4a 100644 --- a/frontend/openapi-ts.config.ts +++ b/frontend/openapi-ts.config.ts @@ -5,29 +5,17 @@ export default defineConfig({ output: "./src/client", plugins: [ - "legacy/axios", + { name: "@hey-api/client-axios", throwOnError: true }, + { name: "@hey-api/typescript", case: "preserve" }, { name: "@hey-api/sdk", - // NOTE: this doesn't allow tree-shaking - asClass: true, - operationId: true, - classNameBuilder: "{{name}}Service", - methodNameBuilder: (operation) => { - // @ts-expect-error - let name: string = operation.name - // @ts-expect-error - const service: string = operation.service - - if (service && name.toLowerCase().startsWith(service.toLowerCase())) { - name = name.slice(service.length) - } - - return name.charAt(0).toLowerCase() + name.slice(1) + operations: { + // NOTE: this doesn't allow tree-shaking + strategy: "byTags", + methods: "static", + containerName: "{{name}}Service", + methodName: (name: string): string => name.replace(/^[^-]*-/, ""), }, }, - { - name: "@hey-api/schemas", - type: "json", - }, ], }) diff --git a/frontend/package.json b/frontend/package.json index dcd7b327..a59f4593 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.16", - "@hey-api/openapi-ts": "0.73.0", + "@hey-api/openapi-ts": "0.97.3", "@playwright/test": "1.61.1", "@tanstack/router-devtools": "^1.167.0", "@tanstack/router-plugin": "^1.168.18", diff --git a/frontend/src/client/client.gen.ts b/frontend/src/client/client.gen.ts new file mode 100644 index 00000000..988ea750 --- /dev/null +++ b/frontend/src/client/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig({ throwOnError: true })); diff --git a/frontend/src/client/client/client.gen.ts b/frontend/src/client/client/client.gen.ts new file mode 100644 index 00000000..d42045a3 --- /dev/null +++ b/frontend/src/client/client/client.gen.ts @@ -0,0 +1,161 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions } from './types.gen'; +import { buildUrl, createConfig, mergeConfigs, mergeHeaders, setAuthParams } from './utils.gen'; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + let instance: AxiosInstance; + + if (_config.axios && !('Axios' in _config.axios)) { + instance = _config.axios; + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { auth, ...configWithoutAuth } = _config; + instance = axios.create(configWithoutAuth); + } + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + instance.defaults = { + ...instance.defaults, + ..._config, + // @ts-expect-error + headers: mergeHeaders(instance.defaults.headers, _config.headers), + }; + return getConfig(); + }; + + const beforeRequest = async < + TData = unknown, + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + axios: options.axios ?? _config.axios ?? instance, + headers: mergeHeaders(_config.headers, options.headers), + }; + + if (opts.security) { + await setAuthParams(opts); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + // @ts-expect-error + const request: Client['request'] = async (options) => { + const { opts, url } = await beforeRequest(options); + try { + // assign Axios here for consistency with fetch + const _axios = opts.axios!; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { auth, ...optsWithoutAuth } = opts; + const response = await _axios({ + ...optsWithoutAuth, + baseURL: '', // the baseURL is already included in `url` + data: getValidRequestBody(opts), + headers: opts.headers as RawAxiosRequestHeaders, + // let `paramsSerializer()` handle query params if it exists + params: opts.paramsSerializer ? opts.query : undefined, + url, + }); + + let { data } = response; + + if (opts.responseType === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return { + ...response, + data: data ?? {}, + }; + } catch (error) { + const e = error as AxiosError; + if (opts.throwOnError) { + throw e; + } + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as Record, + method, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + // @ts-expect-error + signal: opts.signal, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => + buildUrl({ axios: instance, ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + instance, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/frontend/src/client/client/index.ts b/frontend/src/client/client/index.ts new file mode 100644 index 00000000..7bd7b988 --- /dev/null +++ b/frontend/src/client/client/index.ts @@ -0,0 +1,23 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + TDataShape, +} from './types.gen'; +export { createConfig } from './utils.gen'; diff --git a/frontend/src/client/client/types.gen.ts b/frontend/src/client/client/types.gen.ts new file mode 100644 index 00000000..9d4eeb0b --- /dev/null +++ b/frontend/src/client/client/types.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + AxiosError, + AxiosInstance, + AxiosRequestHeaders, + AxiosResponse, + AxiosStatic, + CreateAxiosDefaults, +} from 'axios'; + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; + +export interface Config + extends Omit, CoreConfig { + /** + * Axios implementation. You can use this option to provide either an + * `AxiosStatic` or an `AxiosInstance`. + * + * @default axios + */ + axios?: AxiosStatic | AxiosInstance; + /** + * Base URL for all requests made by this client. + */ + baseURL?: T['baseURL']; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | AxiosRequestHeaders + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ClientOptions { + baseURL?: string; + throwOnError?: boolean; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, +> = ThrowOnError extends true + ? Promise ? TData[keyof TData] : TData>> + : Promise< + | (AxiosResponse ? TData[keyof TData] : TData> & { + error: undefined; + }) + | (AxiosError ? TError[keyof TError] : TError> & { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + }) + >; + +type MethodFn = ( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = ( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & + Pick< + RequestOptions, + 'axios' | 'baseURL' | 'paramsSerializer' | 'querySerializer' + >, +) => string; + +export type Client = CoreClient & { + instance: AxiosInstance; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, +> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & + ([TData] extends [never] ? unknown : Omit); diff --git a/frontend/src/client/client/utils.gen.ts b/frontend/src/client/client/utils.gen.ts new file mode 100644 index 00000000..837b8690 --- /dev/null +++ b/frontend/src/client/client/utils.gen.ts @@ -0,0 +1,206 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +const checkForExistence = ( + options: Pick & { + headers: Record; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if (name in options.headers || options.query?.[name]) { + return true; + } + if ( + 'Cookie' in options.headers && + options.headers['Cookie'] && + typeof options.headers['Cookie'] === 'string' + ) { + return options.headers['Cookie'].includes(`${name}=`); + } + return false; +}; + +export async function setAuthParams( + options: Pick & { + headers: Record; + }, +): Promise { + for (const auth of options.security ?? []) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': { + const value = `${name}=${token}`; + if ('Cookie' in options.headers && options.headers['Cookie']) { + options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`; + } else { + options.headers['Cookie'] = value; + } + break; + } + case 'header': + default: + options.headers[name] = token; + break; + } + } +} + +export const buildUrl: Client['buildUrl'] = (options) => { + const instanceBaseUrl = options.axios?.defaults?.baseURL; + + const baseUrl = + options.baseURL && typeof options.baseURL === 'string' ? options.baseURL : instanceBaseUrl; + + return getUrl({ + baseUrl: baseUrl as string, + path: options.path, + // let `paramsSerializer()` handle query params if it exists + query: !options.paramsSerializer ? options.query : undefined, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); +}; + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +/** + * Special Axios headers keywords allowing to set headers by request method. + */ +export const axiosHeadersKeywords = [ + 'common', + 'delete', + 'get', + 'head', + 'patch', + 'post', + 'put', +] as const; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Record => { + const mergedHeaders: Record = {}; + for (const header of headers) { + if (!header || typeof header !== 'object') { + continue; + } + + const iterator = Object.entries(header); + + for (const [key, value] of iterator) { + if ( + axiosHeadersKeywords.includes(key as (typeof axiosHeadersKeywords)[number]) && + typeof value === 'object' + ) { + mergedHeaders[key] = { + ...(mergedHeaders[key] as Record), + ...value, + }; + } else if (value === null) { + delete mergedHeaders[key]; + } else if (Array.isArray(value)) { + for (const v of value) { + // @ts-expect-error + mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string]; + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders[key] = typeof value === 'object' ? JSON.stringify(value) : (value as string); + } + } + } + return mergedHeaders; +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...override, +}); diff --git a/frontend/src/client/core/ApiError.ts b/frontend/src/client/core/ApiError.ts deleted file mode 100644 index 36675d28..00000000 --- a/frontend/src/client/core/ApiError.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; - -export class ApiError extends Error { - public readonly url: string; - public readonly status: number; - public readonly statusText: string; - public readonly body: unknown; - public readonly request: ApiRequestOptions; - - constructor(request: ApiRequestOptions, response: ApiResult, message: string) { - super(message); - - this.name = 'ApiError'; - this.url = response.url; - this.status = response.status; - this.statusText = response.statusText; - this.body = response.body; - this.request = request; - } -} \ No newline at end of file diff --git a/frontend/src/client/core/ApiRequestOptions.ts b/frontend/src/client/core/ApiRequestOptions.ts deleted file mode 100644 index 939a0aa4..00000000 --- a/frontend/src/client/core/ApiRequestOptions.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type ApiRequestOptions = { - readonly body?: any; - readonly cookies?: Record; - readonly errors?: Record; - readonly formData?: Record | any[] | Blob | File; - readonly headers?: Record; - readonly mediaType?: string; - readonly method: - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT'; - readonly path?: Record; - readonly query?: Record; - readonly responseHeader?: string; - readonly responseTransformer?: (data: unknown) => Promise; - readonly url: string; -}; \ No newline at end of file diff --git a/frontend/src/client/core/ApiResult.ts b/frontend/src/client/core/ApiResult.ts deleted file mode 100644 index 4c58e391..00000000 --- a/frontend/src/client/core/ApiResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type ApiResult = { - readonly body: TData; - readonly ok: boolean; - readonly status: number; - readonly statusText: string; - readonly url: string; -}; \ No newline at end of file diff --git a/frontend/src/client/core/CancelablePromise.ts b/frontend/src/client/core/CancelablePromise.ts deleted file mode 100644 index ccc082e8..00000000 --- a/frontend/src/client/core/CancelablePromise.ts +++ /dev/null @@ -1,126 +0,0 @@ -export class CancelError extends Error { - constructor(message: string) { - super(message); - this.name = 'CancelError'; - } - - public get isCancelled(): boolean { - return true; - } -} - -export interface OnCancel { - readonly isResolved: boolean; - readonly isRejected: boolean; - readonly isCancelled: boolean; - - (cancelHandler: () => void): void; -} - -export class CancelablePromise implements Promise { - private _isResolved: boolean; - private _isRejected: boolean; - private _isCancelled: boolean; - readonly cancelHandlers: (() => void)[]; - readonly promise: Promise; - private _resolve?: (value: T | PromiseLike) => void; - private _reject?: (reason?: unknown) => void; - - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: unknown) => void, - onCancel: OnCancel - ) => void - ) { - this._isResolved = false; - this._isRejected = false; - this._isCancelled = false; - this.cancelHandlers = []; - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - - const onResolve = (value: T | PromiseLike): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isResolved = true; - if (this._resolve) this._resolve(value); - }; - - const onReject = (reason?: unknown): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isRejected = true; - if (this._reject) this._reject(reason); - }; - - const onCancel = (cancelHandler: () => void): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this.cancelHandlers.push(cancelHandler); - }; - - Object.defineProperty(onCancel, 'isResolved', { - get: (): boolean => this._isResolved, - }); - - Object.defineProperty(onCancel, 'isRejected', { - get: (): boolean => this._isRejected, - }); - - Object.defineProperty(onCancel, 'isCancelled', { - get: (): boolean => this._isCancelled, - }); - - return executor(onResolve, onReject, onCancel as OnCancel); - }); - } - - get [Symbol.toStringTag]() { - return "Cancellable Promise"; - } - - public then( - onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onRejected?: ((reason: unknown) => TResult2 | PromiseLike) | null - ): Promise { - return this.promise.then(onFulfilled, onRejected); - } - - public catch( - onRejected?: ((reason: unknown) => TResult | PromiseLike) | null - ): Promise { - return this.promise.catch(onRejected); - } - - public finally(onFinally?: (() => void) | null): Promise { - return this.promise.finally(onFinally); - } - - public cancel(): void { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isCancelled = true; - if (this.cancelHandlers.length) { - try { - for (const cancelHandler of this.cancelHandlers) { - cancelHandler(); - } - } catch (error) { - console.warn('Cancellation threw an error', error); - return; - } - } - this.cancelHandlers.length = 0; - if (this._reject) this._reject(new CancelError('Request aborted')); - } - - public get isCancelled(): boolean { - return this._isCancelled; - } -} \ No newline at end of file diff --git a/frontend/src/client/core/OpenAPI.ts b/frontend/src/client/core/OpenAPI.ts deleted file mode 100644 index 74f92b40..00000000 --- a/frontend/src/client/core/OpenAPI.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { AxiosRequestConfig, AxiosResponse } from 'axios'; -import type { ApiRequestOptions } from './ApiRequestOptions'; - -type Headers = Record; -type Middleware = (value: T) => T | Promise; -type Resolver = (options: ApiRequestOptions) => Promise; - -export class Interceptors { - _fns: Middleware[]; - - constructor() { - this._fns = []; - } - - eject(fn: Middleware): void { - const index = this._fns.indexOf(fn); - if (index !== -1) { - this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]; - } - } - - use(fn: Middleware): void { - this._fns = [...this._fns, fn]; - } -} - -export type OpenAPIConfig = { - BASE: string; - CREDENTIALS: 'include' | 'omit' | 'same-origin'; - ENCODE_PATH?: ((path: string) => string) | undefined; - HEADERS?: Headers | Resolver | undefined; - PASSWORD?: string | Resolver | undefined; - TOKEN?: string | Resolver | undefined; - USERNAME?: string | Resolver | undefined; - VERSION: string; - WITH_CREDENTIALS: boolean; - interceptors: { - request: Interceptors; - response: Interceptors; - }; -}; - -export const OpenAPI: OpenAPIConfig = { - BASE: '', - CREDENTIALS: 'include', - ENCODE_PATH: undefined, - HEADERS: undefined, - PASSWORD: undefined, - TOKEN: undefined, - USERNAME: undefined, - VERSION: '0.1.0', - WITH_CREDENTIALS: false, - interceptors: { - request: new Interceptors(), - response: new Interceptors(), - }, -}; \ No newline at end of file diff --git a/frontend/src/client/core/auth.gen.ts b/frontend/src/client/core/auth.gen.ts new file mode 100644 index 00000000..3ebf9947 --- /dev/null +++ b/frontend/src/client/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/frontend/src/client/core/bodySerializer.gen.ts b/frontend/src/client/core/bodySerializer.gen.ts new file mode 100644 index 00000000..67daca60 --- /dev/null +++ b/frontend/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/frontend/src/client/core/params.gen.ts b/frontend/src/client/core/params.gen.ts new file mode 100644 index 00000000..6478519b --- /dev/null +++ b/frontend/src/client/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: Object.create(null), + headers: Object.create(null), + path: Object.create(null), + query: Object.create(null), + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/frontend/src/client/core/pathSerializer.gen.ts b/frontend/src/client/core/pathSerializer.gen.ts new file mode 100644 index 00000000..994b2848 --- /dev/null +++ b/frontend/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/frontend/src/client/core/queryKeySerializer.gen.ts b/frontend/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 00000000..5000df60 --- /dev/null +++ b/frontend/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/frontend/src/client/core/request.ts b/frontend/src/client/core/request.ts deleted file mode 100644 index ecc2e393..00000000 --- a/frontend/src/client/core/request.ts +++ /dev/null @@ -1,347 +0,0 @@ -import axios from 'axios'; -import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; - -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -export const isString = (value: unknown): value is string => { - return typeof value === 'string'; -}; - -export const isStringWithValue = (value: unknown): value is string => { - return isString(value) && value !== ''; -}; - -export const isBlob = (value: any): value is Blob => { - return value instanceof Blob; -}; - -export const isFormData = (value: unknown): value is FormData => { - return value instanceof FormData; -}; - -export const isSuccess = (status: number): boolean => { - return status >= 200 && status < 300; -}; - -export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString('base64'); - } -}; - -export const getQueryString = (params: Record): string => { - const qs: string[] = []; - - const append = (key: string, value: unknown) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; - - const encodePair = (key: string, value: unknown) => { - if (value === undefined || value === null) { - return; - } - - if (value instanceof Date) { - append(key, value.toISOString()); - } else if (Array.isArray(value)) { - value.forEach(v => encodePair(key, v)); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v)); - } else { - append(key, value); - } - }; - - Object.entries(params).forEach(([key, value]) => encodePair(key, value)); - - return qs.length ? `?${qs.join('&')}` : ''; -}; - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace('{api-version}', config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); - - const url = config.BASE + path; - return options.query ? url + getQueryString(options.query) : url; -}; - -export const getFormData = (options: ApiRequestOptions): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); - - const process = (key: string, value: unknown) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; - - Object.entries(options.formData) - .filter(([, value]) => value !== undefined && value !== null) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; -}; - -type Resolver = (options: ApiRequestOptions) => Promise; - -export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { - if (typeof resolver === 'function') { - return (resolver as Resolver)(options); - } - return resolver; -}; - -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise> => { - const [token, username, password, additionalHeaders] = await Promise.all([ - // @ts-ignore - resolve(options, config.TOKEN), - // @ts-ignore - resolve(options, config.USERNAME), - // @ts-ignore - resolve(options, config.PASSWORD), - // @ts-ignore - resolve(options, config.HEADERS), - ]); - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - }) - .filter(([, value]) => value !== undefined && value !== null) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } else if (options.formData !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } - } - - return headers; -}; - -export const getRequestBody = (options: ApiRequestOptions): unknown => { - if (options.body) { - return options.body; - } - return undefined; -}; - -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: unknown, - formData: FormData | undefined, - headers: Record, - onCancel: OnCancel, - axiosClient: AxiosInstance -): Promise> => { - const controller = new AbortController(); - - let requestConfig: AxiosRequestConfig = { - data: body ?? formData, - headers, - method: options.method, - signal: controller.signal, - url, - withCredentials: config.WITH_CREDENTIALS, - }; - - onCancel(() => controller.abort()); - - for (const fn of config.interceptors.request._fns) { - requestConfig = await fn(requestConfig); - } - - try { - return await axiosClient.request(requestConfig); - } catch (error) { - const axiosError = error as AxiosError; - if (axiosError.response) { - return axiosError.response; - } - throw error; - } -}; - -export const getResponseHeader = (response: AxiosResponse, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers[responseHeader]; - if (isString(content)) { - return content; - } - } - return undefined; -}; - -export const getResponseBody = (response: AxiosResponse): unknown => { - if (response.status !== 204) { - return response.data; - } - return undefined; -}; - -export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { - const errors: Record = { - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 407: 'Proxy Authentication Required', - 408: 'Request Timeout', - 409: 'Conflict', - 410: 'Gone', - 411: 'Length Required', - 412: 'Precondition Failed', - 413: 'Payload Too Large', - 414: 'URI Too Long', - 415: 'Unsupported Media Type', - 416: 'Range Not Satisfiable', - 417: 'Expectation Failed', - 418: 'Im a teapot', - 421: 'Misdirected Request', - 422: 'Unprocessable Content', - 423: 'Locked', - 424: 'Failed Dependency', - 425: 'Too Early', - 426: 'Upgrade Required', - 428: 'Precondition Required', - 429: 'Too Many Requests', - 431: 'Request Header Fields Too Large', - 451: 'Unavailable For Legal Reasons', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', - 505: 'HTTP Version Not Supported', - 506: 'Variant Also Negotiates', - 507: 'Insufficient Storage', - 508: 'Loop Detected', - 510: 'Not Extended', - 511: 'Network Authentication Required', - ...options.errors, - } - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? 'unknown'; - const errorStatusText = result.statusText ?? 'unknown'; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError(options, result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` - ); - } -}; - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @param axiosClient The axios client instance to use - * @returns CancelablePromise - * @throws ApiError - */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options); - - if (!onCancel.isCancelled) { - let response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient); - - for (const fn of config.interceptors.response._fns) { - response = await fn(response); - } - - const responseBody = getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - let transformedBody = responseBody; - if (options.responseTransformer && isSuccess(response.status)) { - transformedBody = await options.responseTransformer(responseBody) - } - - const result: ApiResult = { - url, - ok: isSuccess(response.status), - status: response.status, - statusText: response.statusText, - body: responseHeader ?? transformedBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); -}; \ No newline at end of file diff --git a/frontend/src/client/core/serverSentEvents.gen.ts b/frontend/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 00000000..ddf3c4d1 --- /dev/null +++ b/frontend/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/frontend/src/client/core/types.gen.ts b/frontend/src/client/core/types.gen.ts new file mode 100644 index 00000000..9efe71d4 --- /dev/null +++ b/frontend/src/client/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/frontend/src/client/core/utils.gen.ts b/frontend/src/client/core/utils.gen.ts new file mode 100644 index 00000000..9a4fec78 --- /dev/null +++ b/frontend/src/client/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/frontend/src/client/index.ts b/frontend/src/client/index.ts index 50a1dd73..f4fdf307 100644 --- a/frontend/src/client/index.ts +++ b/frontend/src/client/index.ts @@ -1,6 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI'; -export * from './sdk.gen'; -export * from './types.gen'; \ No newline at end of file + +export { ItemsService, LoginService, type Options, PrivateService, UsersService, UtilsService } from './sdk.gen'; +export type { Body_login_login_access_token, ClientOptions, HTTPValidationError, ItemCreate, ItemPublic, itemsCreateItemData, itemsCreateItemError, itemsCreateItemErrors, itemsCreateItemResponse, itemsCreateItemResponses, itemsDeleteItemData, itemsDeleteItemError, itemsDeleteItemErrors, itemsDeleteItemResponse, itemsDeleteItemResponses, ItemsPublic, itemsReadItemData, itemsReadItemError, itemsReadItemErrors, itemsReadItemResponse, itemsReadItemResponses, itemsReadItemsData, itemsReadItemsError, itemsReadItemsErrors, itemsReadItemsResponse, itemsReadItemsResponses, itemsUpdateItemData, itemsUpdateItemError, itemsUpdateItemErrors, itemsUpdateItemResponse, itemsUpdateItemResponses, ItemUpdate, loginLoginAccessTokenData, loginLoginAccessTokenError, loginLoginAccessTokenErrors, loginLoginAccessTokenResponse, loginLoginAccessTokenResponses, loginRecoverPasswordData, loginRecoverPasswordError, loginRecoverPasswordErrors, loginRecoverPasswordHtmlContentData, loginRecoverPasswordHtmlContentError, loginRecoverPasswordHtmlContentErrors, loginRecoverPasswordHtmlContentResponse, loginRecoverPasswordHtmlContentResponses, loginRecoverPasswordResponse, loginRecoverPasswordResponses, loginResetPasswordData, loginResetPasswordError, loginResetPasswordErrors, loginResetPasswordResponse, loginResetPasswordResponses, loginTestTokenData, loginTestTokenResponse, loginTestTokenResponses, Message, NewPassword, privateCreateUserData, privateCreateUserError, privateCreateUserErrors, privateCreateUserResponse, privateCreateUserResponses, PrivateUserCreate, Token, UpdatePassword, UserCreate, UserPublic, UserRegister, usersCreateUserData, usersCreateUserError, usersCreateUserErrors, usersCreateUserResponse, usersCreateUserResponses, usersDeleteUserData, usersDeleteUserError, usersDeleteUserErrors, usersDeleteUserMeData, usersDeleteUserMeResponse, usersDeleteUserMeResponses, usersDeleteUserResponse, usersDeleteUserResponses, UsersPublic, usersReadUserByIdData, usersReadUserByIdError, usersReadUserByIdErrors, usersReadUserByIdResponse, usersReadUserByIdResponses, usersReadUserMeData, usersReadUserMeResponse, usersReadUserMeResponses, usersReadUsersData, usersReadUsersError, usersReadUsersErrors, usersReadUsersResponse, usersReadUsersResponses, usersRegisterUserData, usersRegisterUserError, usersRegisterUserErrors, usersRegisterUserResponse, usersRegisterUserResponses, usersUpdatePasswordMeData, usersUpdatePasswordMeError, usersUpdatePasswordMeErrors, usersUpdatePasswordMeResponse, usersUpdatePasswordMeResponses, usersUpdateUserData, usersUpdateUserError, usersUpdateUserErrors, usersUpdateUserMeData, usersUpdateUserMeError, usersUpdateUserMeErrors, usersUpdateUserMeResponse, usersUpdateUserMeResponses, usersUpdateUserResponse, usersUpdateUserResponses, UserUpdate, UserUpdateMe, utilsHealthCheckData, utilsHealthCheckResponse, utilsHealthCheckResponses, utilsTestEmailData, utilsTestEmailError, utilsTestEmailErrors, utilsTestEmailResponse, utilsTestEmailResponses, ValidationError } from './types.gen'; diff --git a/frontend/src/client/schemas.gen.ts b/frontend/src/client/schemas.gen.ts deleted file mode 100644 index ea7b4f5c..00000000 --- a/frontend/src/client/schemas.gen.ts +++ /dev/null @@ -1,571 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export const Body_login_login_access_tokenSchema = { - properties: { - grant_type: { - anyOf: [ - { - type: 'string', - pattern: '^password$' - }, - { - type: 'null' - } - ], - title: 'Grant Type' - }, - username: { - type: 'string', - title: 'Username' - }, - password: { - type: 'string', - format: 'password', - title: 'Password' - }, - scope: { - type: 'string', - title: 'Scope', - default: '' - }, - client_id: { - anyOf: [ - { - type: 'string' - }, - { - type: 'null' - } - ], - title: 'Client Id' - }, - client_secret: { - anyOf: [ - { - type: 'string' - }, - { - type: 'null' - } - ], - format: 'password', - title: 'Client Secret' - } - }, - type: 'object', - required: ['username', 'password'], - title: 'Body_login-login_access_token' -} as const; - -export const HTTPValidationErrorSchema = { - properties: { - detail: { - items: { - '$ref': '#/components/schemas/ValidationError' - }, - type: 'array', - title: 'Detail' - } - }, - type: 'object', - title: 'HTTPValidationError' -} as const; - -export const ItemCreateSchema = { - properties: { - title: { - type: 'string', - maxLength: 255, - minLength: 1, - title: 'Title' - }, - description: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Description' - } - }, - type: 'object', - required: ['title'], - title: 'ItemCreate' -} as const; - -export const ItemPublicSchema = { - properties: { - title: { - type: 'string', - maxLength: 255, - minLength: 1, - title: 'Title' - }, - description: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Description' - }, - id: { - type: 'string', - format: 'uuid', - title: 'Id' - }, - owner_id: { - type: 'string', - format: 'uuid', - title: 'Owner Id' - }, - created_at: { - anyOf: [ - { - type: 'string', - format: 'date-time' - }, - { - type: 'null' - } - ], - title: 'Created At' - } - }, - type: 'object', - required: ['title', 'id', 'owner_id'], - title: 'ItemPublic' -} as const; - -export const ItemUpdateSchema = { - properties: { - title: { - anyOf: [ - { - type: 'string', - maxLength: 255, - minLength: 1 - }, - { - type: 'null' - } - ], - title: 'Title' - }, - description: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Description' - } - }, - type: 'object', - title: 'ItemUpdate' -} as const; - -export const ItemsPublicSchema = { - properties: { - data: { - items: { - '$ref': '#/components/schemas/ItemPublic' - }, - type: 'array', - title: 'Data' - }, - count: { - type: 'integer', - title: 'Count' - } - }, - type: 'object', - required: ['data', 'count'], - title: 'ItemsPublic' -} as const; - -export const MessageSchema = { - properties: { - message: { - type: 'string', - title: 'Message' - } - }, - type: 'object', - required: ['message'], - title: 'Message' -} as const; - -export const NewPasswordSchema = { - properties: { - token: { - type: 'string', - title: 'Token' - }, - new_password: { - type: 'string', - maxLength: 128, - minLength: 8, - title: 'New Password' - } - }, - type: 'object', - required: ['token', 'new_password'], - title: 'NewPassword' -} as const; - -export const PrivateUserCreateSchema = { - properties: { - email: { - type: 'string', - title: 'Email' - }, - password: { - type: 'string', - title: 'Password' - }, - full_name: { - type: 'string', - title: 'Full Name' - }, - is_verified: { - type: 'boolean', - title: 'Is Verified', - default: false - } - }, - type: 'object', - required: ['email', 'password', 'full_name'], - title: 'PrivateUserCreate' -} as const; - -export const TokenSchema = { - properties: { - access_token: { - type: 'string', - title: 'Access Token' - }, - token_type: { - type: 'string', - title: 'Token Type', - default: 'bearer' - } - }, - type: 'object', - required: ['access_token'], - title: 'Token' -} as const; - -export const UpdatePasswordSchema = { - properties: { - current_password: { - type: 'string', - maxLength: 128, - minLength: 8, - title: 'Current Password' - }, - new_password: { - type: 'string', - maxLength: 128, - minLength: 8, - title: 'New Password' - } - }, - type: 'object', - required: ['current_password', 'new_password'], - title: 'UpdatePassword' -} as const; - -export const UserCreateSchema = { - properties: { - email: { - type: 'string', - maxLength: 255, - format: 'email', - title: 'Email' - }, - is_active: { - type: 'boolean', - title: 'Is Active', - default: true - }, - is_superuser: { - type: 'boolean', - title: 'Is Superuser', - default: false - }, - full_name: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Full Name' - }, - password: { - type: 'string', - maxLength: 128, - minLength: 8, - title: 'Password' - } - }, - type: 'object', - required: ['email', 'password'], - title: 'UserCreate' -} as const; - -export const UserPublicSchema = { - properties: { - email: { - type: 'string', - maxLength: 255, - format: 'email', - title: 'Email' - }, - is_active: { - type: 'boolean', - title: 'Is Active', - default: true - }, - is_superuser: { - type: 'boolean', - title: 'Is Superuser', - default: false - }, - full_name: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Full Name' - }, - id: { - type: 'string', - format: 'uuid', - title: 'Id' - }, - created_at: { - anyOf: [ - { - type: 'string', - format: 'date-time' - }, - { - type: 'null' - } - ], - title: 'Created At' - } - }, - type: 'object', - required: ['email', 'id'], - title: 'UserPublic' -} as const; - -export const UserRegisterSchema = { - properties: { - email: { - type: 'string', - maxLength: 255, - format: 'email', - title: 'Email' - }, - password: { - type: 'string', - maxLength: 128, - minLength: 8, - title: 'Password' - }, - full_name: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Full Name' - } - }, - type: 'object', - required: ['email', 'password'], - title: 'UserRegister' -} as const; - -export const UserUpdateSchema = { - properties: { - email: { - anyOf: [ - { - type: 'string', - maxLength: 255, - format: 'email' - }, - { - type: 'null' - } - ], - title: 'Email' - }, - is_active: { - anyOf: [ - { - type: 'boolean' - }, - { - type: 'null' - } - ], - title: 'Is Active' - }, - is_superuser: { - anyOf: [ - { - type: 'boolean' - }, - { - type: 'null' - } - ], - title: 'Is Superuser' - }, - full_name: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Full Name' - }, - password: { - anyOf: [ - { - type: 'string', - maxLength: 128, - minLength: 8 - }, - { - type: 'null' - } - ], - title: 'Password' - } - }, - type: 'object', - title: 'UserUpdate' -} as const; - -export const UserUpdateMeSchema = { - properties: { - full_name: { - anyOf: [ - { - type: 'string', - maxLength: 255 - }, - { - type: 'null' - } - ], - title: 'Full Name' - }, - email: { - anyOf: [ - { - type: 'string', - maxLength: 255, - format: 'email' - }, - { - type: 'null' - } - ], - title: 'Email' - } - }, - type: 'object', - title: 'UserUpdateMe' -} as const; - -export const UsersPublicSchema = { - properties: { - data: { - items: { - '$ref': '#/components/schemas/UserPublic' - }, - type: 'array', - title: 'Data' - }, - count: { - type: 'integer', - title: 'Count' - } - }, - type: 'object', - required: ['data', 'count'], - title: 'UsersPublic' -} as const; - -export const ValidationErrorSchema = { - properties: { - loc: { - items: { - anyOf: [ - { - type: 'string' - }, - { - type: 'integer' - } - ] - }, - type: 'array', - title: 'Location' - }, - msg: { - type: 'string', - title: 'Message' - }, - type: { - type: 'string', - title: 'Error Type' - }, - input: { - title: 'Input' - }, - ctx: { - type: 'object', - title: 'Context' - } - }, - type: 'object', - required: ['loc', 'msg', 'type'], - title: 'ValidationError' -} as const; \ No newline at end of file diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index ba79e3f7..b1dd2cf1 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -1,236 +1,97 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { CancelablePromise } from './core/CancelablePromise'; -import { OpenAPI } from './core/OpenAPI'; -import { request as __request } from './core/request'; -import type { ItemsReadItemsData, ItemsReadItemsResponse, ItemsCreateItemData, ItemsCreateItemResponse, ItemsReadItemData, ItemsReadItemResponse, ItemsUpdateItemData, ItemsUpdateItemResponse, ItemsDeleteItemData, ItemsDeleteItemResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen'; +import { type Client, type Options as Options2, type TDataShape, urlSearchParamsBodySerializer } from './client'; +import { client } from './client.gen'; +import type { itemsCreateItemData, itemsCreateItemErrors, itemsCreateItemResponses, itemsDeleteItemData, itemsDeleteItemErrors, itemsDeleteItemResponses, itemsReadItemData, itemsReadItemErrors, itemsReadItemResponses, itemsReadItemsData, itemsReadItemsErrors, itemsReadItemsResponses, itemsUpdateItemData, itemsUpdateItemErrors, itemsUpdateItemResponses, loginLoginAccessTokenData, loginLoginAccessTokenErrors, loginLoginAccessTokenResponses, loginRecoverPasswordData, loginRecoverPasswordErrors, loginRecoverPasswordHtmlContentData, loginRecoverPasswordHtmlContentErrors, loginRecoverPasswordHtmlContentResponses, loginRecoverPasswordResponses, loginResetPasswordData, loginResetPasswordErrors, loginResetPasswordResponses, loginTestTokenData, loginTestTokenResponses, privateCreateUserData, privateCreateUserErrors, privateCreateUserResponses, usersCreateUserData, usersCreateUserErrors, usersCreateUserResponses, usersDeleteUserData, usersDeleteUserErrors, usersDeleteUserMeData, usersDeleteUserMeResponses, usersDeleteUserResponses, usersReadUserByIdData, usersReadUserByIdErrors, usersReadUserByIdResponses, usersReadUserMeData, usersReadUserMeResponses, usersReadUsersData, usersReadUsersErrors, usersReadUsersResponses, usersRegisterUserData, usersRegisterUserErrors, usersRegisterUserResponses, usersUpdatePasswordMeData, usersUpdatePasswordMeErrors, usersUpdatePasswordMeResponses, usersUpdateUserData, usersUpdateUserErrors, usersUpdateUserMeData, usersUpdateUserMeErrors, usersUpdateUserMeResponses, usersUpdateUserResponses, utilsHealthCheckData, utilsHealthCheckResponses, utilsTestEmailData, utilsTestEmailErrors, utilsTestEmailResponses } from './types.gen'; -export class ItemsService { +export type Options = Options2 & { /** - * Read Items - * Retrieve items. - * @param data The data for the request. - * @param data.skip - * @param data.limit - * @returns ItemsPublic Successful Response - * @throws ApiError + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. */ - public static readItems(data: ItemsReadItemsData = {}): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/items/', - query: { - skip: data.skip, - limit: data.limit - }, - errors: { - 422: 'Validation Error' - } - }); - } - + client?: Client; /** - * Create Item - * Create new item. - * @param data The data for the request. - * @param data.requestBody - * @returns ItemPublic Successful Response - * @throws ApiError + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. */ - public static createItem(data: ItemsCreateItemData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/items/', - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' - } - }); - } - - /** - * Read Item - * Get item by ID. - * @param data The data for the request. - * @param data.id - * @returns ItemPublic Successful Response - * @throws ApiError - */ - public static readItem(data: ItemsReadItemData): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/items/{id}', - path: { - id: data.id - }, - errors: { - 422: 'Validation Error' - } - }); - } - - /** - * Update Item - * Update an item. - * @param data The data for the request. - * @param data.id - * @param data.requestBody - * @returns ItemPublic Successful Response - * @throws ApiError - */ - public static updateItem(data: ItemsUpdateItemData): CancelablePromise { - return __request(OpenAPI, { - method: 'PUT', - url: '/api/v1/items/{id}', - path: { - id: data.id - }, - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' - } - }); - } - - /** - * Delete Item - * Delete an item. - * @param data The data for the request. - * @param data.id - * @returns Message Successful Response - * @throws ApiError - */ - public static deleteItem(data: ItemsDeleteItemData): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', - url: '/api/v1/items/{id}', - path: { - id: data.id - }, - errors: { - 422: 'Validation Error' - } - }); - } -} + meta?: Record; +}; export class LoginService { /** * Login Access Token + * * OAuth2 compatible token login, get an access token for future requests - * @param data The data for the request. - * @param data.formData - * @returns Token Successful Response - * @throws ApiError */ - public static loginAccessToken(data: LoginLoginAccessTokenData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', + public static loginAccessToken(options: Options) { + return (options.client ?? client).post({ + ...urlSearchParamsBodySerializer, + responseType: 'json', url: '/api/v1/login/access-token', - formData: data.formData, - mediaType: 'application/x-www-form-urlencoded', - errors: { - 422: 'Validation Error' + ...options, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...options.headers } }); } /** * Test Token + * * Test access token - * @returns UserPublic Successful Response - * @throws ApiError */ - public static testToken(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/login/test-token' + public static testToken(options?: Options) { + return (options?.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/login/test-token', + ...options }); } /** * Recover Password + * * Password Recovery - * @param data The data for the request. - * @param data.email - * @returns Message Successful Response - * @throws ApiError */ - public static recoverPassword(data: LoginRecoverPasswordData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', + public static recoverPassword(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', url: '/api/v1/password-recovery/{email}', - path: { - email: data.email - }, - errors: { - 422: 'Validation Error' - } + ...options }); } /** * Reset Password + * * Reset password - * @param data The data for the request. - * @param data.requestBody - * @returns Message Successful Response - * @throws ApiError */ - public static resetPassword(data: LoginResetPasswordData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', + public static resetPassword(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', url: '/api/v1/reset-password/', - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers } }); } /** * Recover Password Html Content + * * HTML Content for Password Recovery - * @param data The data for the request. - * @param data.email - * @returns string Successful Response - * @throws ApiError */ - public static recoverPasswordHtmlContent(data: LoginRecoverPasswordHtmlContentData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', + public static recoverPasswordHtmlContent(options: Options) { + return (options.client ?? client).post({ + responseType: 'text', + security: [{ scheme: 'bearer', type: 'http' }], url: '/api/v1/password-recovery-html-content/{email}', - path: { - email: data.email - }, - errors: { - 422: 'Validation Error' - } - }); - } -} - -export class PrivateService { - /** - * Create User - * Create a new user. - * @param data The data for the request. - * @param data.requestBody - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static createUser(data: PrivateCreateUserData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/private/users/', - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' - } + ...options }); } } @@ -238,195 +99,159 @@ export class PrivateService { export class UsersService { /** * Read Users + * * Retrieve users. - * @param data The data for the request. - * @param data.skip - * @param data.limit - * @returns UsersPublic Successful Response - * @throws ApiError */ - public static readUsers(data: UsersReadUsersData = {}): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', + public static readUsers(options?: Options) { + return (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], url: '/api/v1/users/', - query: { - skip: data.skip, - limit: data.limit - }, - errors: { - 422: 'Validation Error' - } + ...options }); } /** * Create User + * * Create new user. - * @param data The data for the request. - * @param data.requestBody - * @returns UserPublic Successful Response - * @throws ApiError */ - public static createUser(data: UsersCreateUserData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', + public static createUser(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], url: '/api/v1/users/', - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers } }); } /** - * Read User Me - * Get current user. - * @returns UserPublic Successful Response - * @throws ApiError + * Delete User Me + * + * Delete own user. */ - public static readUserMe(): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/users/me' + public static deleteUserMe(options?: Options) { + return (options?.client ?? client).delete({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/users/me', + ...options }); } /** - * Delete User Me - * Delete own user. - * @returns Message Successful Response - * @throws ApiError + * Read User Me + * + * Get current user. */ - public static deleteUserMe(): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', - url: '/api/v1/users/me' + public static readUserMe(options?: Options) { + return (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/users/me', + ...options }); } /** * Update User Me + * * Update own user. - * @param data The data for the request. - * @param data.requestBody - * @returns UserPublic Successful Response - * @throws ApiError */ - public static updateUserMe(data: UsersUpdateUserMeData): CancelablePromise { - return __request(OpenAPI, { - method: 'PATCH', + public static updateUserMe(options: Options) { + return (options.client ?? client).patch({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], url: '/api/v1/users/me', - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers } }); } /** * Update Password Me + * * Update own password. - * @param data The data for the request. - * @param data.requestBody - * @returns Message Successful Response - * @throws ApiError */ - public static updatePasswordMe(data: UsersUpdatePasswordMeData): CancelablePromise { - return __request(OpenAPI, { - method: 'PATCH', + public static updatePasswordMe(options: Options) { + return (options.client ?? client).patch({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], url: '/api/v1/users/me/password', - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers } }); } /** * Register User + * * Create new user without the need to be logged in. - * @param data The data for the request. - * @param data.requestBody - * @returns UserPublic Successful Response - * @throws ApiError */ - public static registerUser(data: UsersRegisterUserData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', + public static registerUser(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', url: '/api/v1/users/signup', - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' - } - }); - } - - /** - * Read User By Id - * Get a specific user by id. - * @param data The data for the request. - * @param data.userId - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static readUserById(data: UsersReadUserByIdData): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/users/{user_id}', - path: { - user_id: data.userId - }, - errors: { - 422: 'Validation Error' - } - }); - } - - /** - * Update User - * Update a user. - * @param data The data for the request. - * @param data.userId - * @param data.requestBody - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static updateUser(data: UsersUpdateUserData): CancelablePromise { - return __request(OpenAPI, { - method: 'PATCH', - url: '/api/v1/users/{user_id}', - path: { - user_id: data.userId - }, - body: data.requestBody, - mediaType: 'application/json', - errors: { - 422: 'Validation Error' + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers } }); } /** * Delete User + * * Delete a user. - * @param data The data for the request. - * @param data.userId - * @returns Message Successful Response - * @throws ApiError */ - public static deleteUser(data: UsersDeleteUserData): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', + public static deleteUser(options: Options) { + return (options.client ?? client).delete({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], url: '/api/v1/users/{user_id}', - path: { - user_id: data.userId - }, - errors: { - 422: 'Validation Error' + ...options + }); + } + + /** + * Read User By Id + * + * Get a specific user by id. + */ + public static readUserById(options: Options) { + return (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/users/{user_id}', + ...options + }); + } + + /** + * Update User + * + * Update a user. + */ + public static updateUser(options: Options) { + return (options.client ?? client).patch({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/users/{user_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers } }); } @@ -435,34 +260,125 @@ export class UsersService { export class UtilsService { /** * Test Email + * * Test emails. - * @param data The data for the request. - * @param data.emailTo - * @returns Message Successful Response - * @throws ApiError */ - public static testEmail(data: UtilsTestEmailData): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', + public static testEmail(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], url: '/api/v1/utils/test-email/', - query: { - email_to: data.emailTo - }, - errors: { - 422: 'Validation Error' - } + ...options }); } /** * Health Check - * @returns boolean Successful Response - * @throws ApiError */ - public static healthCheck(): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/utils/health-check/' + public static healthCheck(options?: Options) { + return (options?.client ?? client).get({ + responseType: 'json', + url: '/api/v1/utils/health-check/', + ...options }); } -} \ No newline at end of file +} + +export class ItemsService { + /** + * Read Items + * + * Retrieve items. + */ + public static readItems(options?: Options) { + return (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/items/', + ...options + }); + } + + /** + * Create Item + * + * Create new item. + */ + public static createItem(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/items/', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } + + /** + * Delete Item + * + * Delete an item. + */ + public static deleteItem(options: Options) { + return (options.client ?? client).delete({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/items/{id}', + ...options + }); + } + + /** + * Read Item + * + * Get item by ID. + */ + public static readItem(options: Options) { + return (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/items/{id}', + ...options + }); + } + + /** + * Update Item + * + * Update an item. + */ + public static updateItem(options: Options) { + return (options.client ?? client).put({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/items/{id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } +} + +export class PrivateService { + /** + * Create User + * + * Create a new user. + */ + public static createUser(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', + url: '/api/v1/private/users/', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } +} diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 596b8fc4..6252d88c 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -1,240 +1,947 @@ // This file is auto-generated by @hey-api/openapi-ts -export type Body_login_login_access_token = { - grant_type?: (string | null); - username: string; - password: string; - scope?: string; - client_id?: (string | null); - client_secret?: (string | null); +export type ClientOptions = { + baseURL: `${string}://${string}` | (string & {}); }; +/** + * Body_login-login_access_token + */ +export type Body_login_login_access_token = { + /** + * Grant Type + */ + grant_type?: string | null; + /** + * Username + */ + username: string; + /** + * Password + */ + password: string; + /** + * Scope + */ + scope?: string; + /** + * Client Id + */ + client_id?: string | null; + /** + * Client Secret + */ + client_secret?: string | null; +}; + +/** + * HTTPValidationError + */ export type HTTPValidationError = { + /** + * Detail + */ detail?: Array; }; +/** + * ItemCreate + */ export type ItemCreate = { + /** + * Title + */ title: string; - description?: (string | null); + /** + * Description + */ + description?: string | null; }; +/** + * ItemPublic + */ export type ItemPublic = { + /** + * Title + */ title: string; - description?: (string | null); + /** + * Description + */ + description?: string | null; + /** + * Id + */ id: string; + /** + * Owner Id + */ owner_id: string; - created_at?: (string | null); + /** + * Created At + */ + created_at?: string | null; }; +/** + * ItemUpdate + */ +export type ItemUpdate = { + /** + * Title + */ + title?: string | null; + /** + * Description + */ + description?: string | null; +}; + +/** + * ItemsPublic + */ export type ItemsPublic = { + /** + * Data + */ data: Array; + /** + * Count + */ count: number; }; -export type ItemUpdate = { - title?: (string | null); - description?: (string | null); -}; - +/** + * Message + */ export type Message = { + /** + * Message + */ message: string; }; +/** + * NewPassword + */ export type NewPassword = { + /** + * Token + */ token: string; + /** + * New Password + */ new_password: string; }; +/** + * PrivateUserCreate + */ export type PrivateUserCreate = { + /** + * Email + */ email: string; + /** + * Password + */ password: string; + /** + * Full Name + */ full_name: string; + /** + * Is Verified + */ is_verified?: boolean; }; +/** + * Token + */ export type Token = { + /** + * Access Token + */ access_token: string; + /** + * Token Type + */ token_type?: string; }; +/** + * UpdatePassword + */ export type UpdatePassword = { + /** + * Current Password + */ current_password: string; + /** + * New Password + */ new_password: string; }; +/** + * UserCreate + */ export type UserCreate = { + /** + * Email + */ email: string; + /** + * Is Active + */ is_active?: boolean; + /** + * Is Superuser + */ is_superuser?: boolean; - full_name?: (string | null); + /** + * Full Name + */ + full_name?: string | null; + /** + * Password + */ password: string; }; +/** + * UserPublic + */ export type UserPublic = { + /** + * Email + */ email: string; + /** + * Is Active + */ is_active?: boolean; + /** + * Is Superuser + */ is_superuser?: boolean; - full_name?: (string | null); + /** + * Full Name + */ + full_name?: string | null; + /** + * Id + */ id: string; - created_at?: (string | null); + /** + * Created At + */ + created_at?: string | null; }; +/** + * UserRegister + */ export type UserRegister = { + /** + * Email + */ email: string; + /** + * Password + */ password: string; - full_name?: (string | null); + /** + * Full Name + */ + full_name?: string | null; }; +/** + * UserUpdate + */ +export type UserUpdate = { + /** + * Email + */ + email?: string | null; + /** + * Is Active + */ + is_active?: boolean | null; + /** + * Is Superuser + */ + is_superuser?: boolean | null; + /** + * Full Name + */ + full_name?: string | null; + /** + * Password + */ + password?: string | null; +}; + +/** + * UserUpdateMe + */ +export type UserUpdateMe = { + /** + * Full Name + */ + full_name?: string | null; + /** + * Email + */ + email?: string | null; +}; + +/** + * UsersPublic + */ export type UsersPublic = { + /** + * Data + */ data: Array; + /** + * Count + */ count: number; }; -export type UserUpdate = { - email?: (string | null); - is_active?: (boolean | null); - is_superuser?: (boolean | null); - full_name?: (string | null); - password?: (string | null); -}; - -export type UserUpdateMe = { - full_name?: (string | null); - email?: (string | null); -}; - +/** + * ValidationError + */ export type ValidationError = { - loc: Array<(string | number)>; + /** + * Location + */ + loc: Array; + /** + * Message + */ msg: string; + /** + * Error Type + */ type: string; + /** + * Input + */ input?: unknown; + /** + * Context + */ ctx?: { [key: string]: unknown; }; }; -export type ItemsReadItemsData = { - limit?: number; - skip?: number; +export type loginLoginAccessTokenData = { + body: Body_login_login_access_token; + path?: never; + query?: never; + url: '/api/v1/login/access-token'; }; -export type ItemsReadItemsResponse = (ItemsPublic); - -export type ItemsCreateItemData = { - requestBody: ItemCreate; +export type loginLoginAccessTokenErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; }; -export type ItemsCreateItemResponse = (ItemPublic); +export type loginLoginAccessTokenError = loginLoginAccessTokenErrors[keyof loginLoginAccessTokenErrors]; -export type ItemsReadItemData = { - id: string; +export type loginLoginAccessTokenResponses = { + /** + * Successful Response + */ + 200: Token; }; -export type ItemsReadItemResponse = (ItemPublic); +export type loginLoginAccessTokenResponse = loginLoginAccessTokenResponses[keyof loginLoginAccessTokenResponses]; -export type ItemsUpdateItemData = { - id: string; - requestBody: ItemUpdate; +export type loginTestTokenData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/login/test-token'; }; -export type ItemsUpdateItemResponse = (ItemPublic); - -export type ItemsDeleteItemData = { - id: string; +export type loginTestTokenResponses = { + /** + * Successful Response + */ + 200: UserPublic; }; -export type ItemsDeleteItemResponse = (Message); +export type loginTestTokenResponse = loginTestTokenResponses[keyof loginTestTokenResponses]; -export type LoginLoginAccessTokenData = { - formData: Body_login_login_access_token; +export type loginRecoverPasswordData = { + body?: never; + path: { + /** + * Email + */ + email: string; + }; + query?: never; + url: '/api/v1/password-recovery/{email}'; }; -export type LoginLoginAccessTokenResponse = (Token); - -export type LoginTestTokenResponse = (UserPublic); - -export type LoginRecoverPasswordData = { - email: string; +export type loginRecoverPasswordErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; }; -export type LoginRecoverPasswordResponse = (Message); +export type loginRecoverPasswordError = loginRecoverPasswordErrors[keyof loginRecoverPasswordErrors]; -export type LoginResetPasswordData = { - requestBody: NewPassword; +export type loginRecoverPasswordResponses = { + /** + * Successful Response + */ + 200: Message; }; -export type LoginResetPasswordResponse = (Message); +export type loginRecoverPasswordResponse = loginRecoverPasswordResponses[keyof loginRecoverPasswordResponses]; -export type LoginRecoverPasswordHtmlContentData = { - email: string; +export type loginResetPasswordData = { + body: NewPassword; + path?: never; + query?: never; + url: '/api/v1/reset-password/'; }; -export type LoginRecoverPasswordHtmlContentResponse = (string); - -export type PrivateCreateUserData = { - requestBody: PrivateUserCreate; +export type loginResetPasswordErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; }; -export type PrivateCreateUserResponse = (UserPublic); +export type loginResetPasswordError = loginResetPasswordErrors[keyof loginResetPasswordErrors]; -export type UsersReadUsersData = { - limit?: number; - skip?: number; +export type loginResetPasswordResponses = { + /** + * Successful Response + */ + 200: Message; }; -export type UsersReadUsersResponse = (UsersPublic); +export type loginResetPasswordResponse = loginResetPasswordResponses[keyof loginResetPasswordResponses]; -export type UsersCreateUserData = { - requestBody: UserCreate; +export type loginRecoverPasswordHtmlContentData = { + body?: never; + path: { + /** + * Email + */ + email: string; + }; + query?: never; + url: '/api/v1/password-recovery-html-content/{email}'; }; -export type UsersCreateUserResponse = (UserPublic); - -export type UsersReadUserMeResponse = (UserPublic); - -export type UsersDeleteUserMeResponse = (Message); - -export type UsersUpdateUserMeData = { - requestBody: UserUpdateMe; +export type loginRecoverPasswordHtmlContentErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; }; -export type UsersUpdateUserMeResponse = (UserPublic); +export type loginRecoverPasswordHtmlContentError = loginRecoverPasswordHtmlContentErrors[keyof loginRecoverPasswordHtmlContentErrors]; -export type UsersUpdatePasswordMeData = { - requestBody: UpdatePassword; +export type loginRecoverPasswordHtmlContentResponses = { + /** + * Successful Response + */ + 200: string; }; -export type UsersUpdatePasswordMeResponse = (Message); +export type loginRecoverPasswordHtmlContentResponse = loginRecoverPasswordHtmlContentResponses[keyof loginRecoverPasswordHtmlContentResponses]; -export type UsersRegisterUserData = { - requestBody: UserRegister; +export type usersReadUsersData = { + body?: never; + path?: never; + query?: { + /** + * Skip + */ + skip?: number; + /** + * Limit + */ + limit?: number; + }; + url: '/api/v1/users/'; }; -export type UsersRegisterUserResponse = (UserPublic); - -export type UsersReadUserByIdData = { - userId: string; +export type usersReadUsersErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; }; -export type UsersReadUserByIdResponse = (UserPublic); +export type usersReadUsersError = usersReadUsersErrors[keyof usersReadUsersErrors]; -export type UsersUpdateUserData = { - requestBody: UserUpdate; - userId: string; +export type usersReadUsersResponses = { + /** + * Successful Response + */ + 200: UsersPublic; }; -export type UsersUpdateUserResponse = (UserPublic); +export type usersReadUsersResponse = usersReadUsersResponses[keyof usersReadUsersResponses]; -export type UsersDeleteUserData = { - userId: string; +export type usersCreateUserData = { + body: UserCreate; + path?: never; + query?: never; + url: '/api/v1/users/'; }; -export type UsersDeleteUserResponse = (Message); - -export type UtilsTestEmailData = { - emailTo: string; +export type usersCreateUserErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; }; -export type UtilsTestEmailResponse = (Message); +export type usersCreateUserError = usersCreateUserErrors[keyof usersCreateUserErrors]; -export type UtilsHealthCheckResponse = (boolean); \ No newline at end of file +export type usersCreateUserResponses = { + /** + * Successful Response + */ + 200: UserPublic; +}; + +export type usersCreateUserResponse = usersCreateUserResponses[keyof usersCreateUserResponses]; + +export type usersDeleteUserMeData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/users/me'; +}; + +export type usersDeleteUserMeResponses = { + /** + * Successful Response + */ + 200: Message; +}; + +export type usersDeleteUserMeResponse = usersDeleteUserMeResponses[keyof usersDeleteUserMeResponses]; + +export type usersReadUserMeData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/users/me'; +}; + +export type usersReadUserMeResponses = { + /** + * Successful Response + */ + 200: UserPublic; +}; + +export type usersReadUserMeResponse = usersReadUserMeResponses[keyof usersReadUserMeResponses]; + +export type usersUpdateUserMeData = { + body: UserUpdateMe; + path?: never; + query?: never; + url: '/api/v1/users/me'; +}; + +export type usersUpdateUserMeErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type usersUpdateUserMeError = usersUpdateUserMeErrors[keyof usersUpdateUserMeErrors]; + +export type usersUpdateUserMeResponses = { + /** + * Successful Response + */ + 200: UserPublic; +}; + +export type usersUpdateUserMeResponse = usersUpdateUserMeResponses[keyof usersUpdateUserMeResponses]; + +export type usersUpdatePasswordMeData = { + body: UpdatePassword; + path?: never; + query?: never; + url: '/api/v1/users/me/password'; +}; + +export type usersUpdatePasswordMeErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type usersUpdatePasswordMeError = usersUpdatePasswordMeErrors[keyof usersUpdatePasswordMeErrors]; + +export type usersUpdatePasswordMeResponses = { + /** + * Successful Response + */ + 200: Message; +}; + +export type usersUpdatePasswordMeResponse = usersUpdatePasswordMeResponses[keyof usersUpdatePasswordMeResponses]; + +export type usersRegisterUserData = { + body: UserRegister; + path?: never; + query?: never; + url: '/api/v1/users/signup'; +}; + +export type usersRegisterUserErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type usersRegisterUserError = usersRegisterUserErrors[keyof usersRegisterUserErrors]; + +export type usersRegisterUserResponses = { + /** + * Successful Response + */ + 200: UserPublic; +}; + +export type usersRegisterUserResponse = usersRegisterUserResponses[keyof usersRegisterUserResponses]; + +export type usersDeleteUserData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/users/{user_id}'; +}; + +export type usersDeleteUserErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type usersDeleteUserError = usersDeleteUserErrors[keyof usersDeleteUserErrors]; + +export type usersDeleteUserResponses = { + /** + * Successful Response + */ + 200: Message; +}; + +export type usersDeleteUserResponse = usersDeleteUserResponses[keyof usersDeleteUserResponses]; + +export type usersReadUserByIdData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/users/{user_id}'; +}; + +export type usersReadUserByIdErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type usersReadUserByIdError = usersReadUserByIdErrors[keyof usersReadUserByIdErrors]; + +export type usersReadUserByIdResponses = { + /** + * Successful Response + */ + 200: UserPublic; +}; + +export type usersReadUserByIdResponse = usersReadUserByIdResponses[keyof usersReadUserByIdResponses]; + +export type usersUpdateUserData = { + body: UserUpdate; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/users/{user_id}'; +}; + +export type usersUpdateUserErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type usersUpdateUserError = usersUpdateUserErrors[keyof usersUpdateUserErrors]; + +export type usersUpdateUserResponses = { + /** + * Successful Response + */ + 200: UserPublic; +}; + +export type usersUpdateUserResponse = usersUpdateUserResponses[keyof usersUpdateUserResponses]; + +export type utilsTestEmailData = { + body?: never; + path?: never; + query: { + /** + * Email To + */ + email_to: string; + }; + url: '/api/v1/utils/test-email/'; +}; + +export type utilsTestEmailErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type utilsTestEmailError = utilsTestEmailErrors[keyof utilsTestEmailErrors]; + +export type utilsTestEmailResponses = { + /** + * Successful Response + */ + 201: Message; +}; + +export type utilsTestEmailResponse = utilsTestEmailResponses[keyof utilsTestEmailResponses]; + +export type utilsHealthCheckData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/utils/health-check/'; +}; + +export type utilsHealthCheckResponses = { + /** + * Response Utils-Health Check + * + * Successful Response + */ + 200: boolean; +}; + +export type utilsHealthCheckResponse = utilsHealthCheckResponses[keyof utilsHealthCheckResponses]; + +export type itemsReadItemsData = { + body?: never; + path?: never; + query?: { + /** + * Skip + */ + skip?: number; + /** + * Limit + */ + limit?: number; + }; + url: '/api/v1/items/'; +}; + +export type itemsReadItemsErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type itemsReadItemsError = itemsReadItemsErrors[keyof itemsReadItemsErrors]; + +export type itemsReadItemsResponses = { + /** + * Successful Response + */ + 200: ItemsPublic; +}; + +export type itemsReadItemsResponse = itemsReadItemsResponses[keyof itemsReadItemsResponses]; + +export type itemsCreateItemData = { + body: ItemCreate; + path?: never; + query?: never; + url: '/api/v1/items/'; +}; + +export type itemsCreateItemErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type itemsCreateItemError = itemsCreateItemErrors[keyof itemsCreateItemErrors]; + +export type itemsCreateItemResponses = { + /** + * Successful Response + */ + 200: ItemPublic; +}; + +export type itemsCreateItemResponse = itemsCreateItemResponses[keyof itemsCreateItemResponses]; + +export type itemsDeleteItemData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/items/{id}'; +}; + +export type itemsDeleteItemErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type itemsDeleteItemError = itemsDeleteItemErrors[keyof itemsDeleteItemErrors]; + +export type itemsDeleteItemResponses = { + /** + * Successful Response + */ + 200: Message; +}; + +export type itemsDeleteItemResponse = itemsDeleteItemResponses[keyof itemsDeleteItemResponses]; + +export type itemsReadItemData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/items/{id}'; +}; + +export type itemsReadItemErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type itemsReadItemError = itemsReadItemErrors[keyof itemsReadItemErrors]; + +export type itemsReadItemResponses = { + /** + * Successful Response + */ + 200: ItemPublic; +}; + +export type itemsReadItemResponse = itemsReadItemResponses[keyof itemsReadItemResponses]; + +export type itemsUpdateItemData = { + body: ItemUpdate; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/items/{id}'; +}; + +export type itemsUpdateItemErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type itemsUpdateItemError = itemsUpdateItemErrors[keyof itemsUpdateItemErrors]; + +export type itemsUpdateItemResponses = { + /** + * Successful Response + */ + 200: ItemPublic; +}; + +export type itemsUpdateItemResponse = itemsUpdateItemResponses[keyof itemsUpdateItemResponses]; + +export type privateCreateUserData = { + body: PrivateUserCreate; + path?: never; + query?: never; + url: '/api/v1/private/users/'; +}; + +export type privateCreateUserErrors = { + /** + * Validation Error + */ + 422: HTTPValidationError; +}; + +export type privateCreateUserError = privateCreateUserErrors[keyof privateCreateUserErrors]; + +export type privateCreateUserResponses = { + /** + * Successful Response + */ + 200: UserPublic; +}; + +export type privateCreateUserResponse = privateCreateUserResponses[keyof privateCreateUserResponses]; diff --git a/frontend/src/components/Admin/AddUser.tsx b/frontend/src/components/Admin/AddUser.tsx index a0b534bd..0cb83b2d 100644 --- a/frontend/src/components/Admin/AddUser.tsx +++ b/frontend/src/components/Admin/AddUser.tsx @@ -72,8 +72,7 @@ const AddUser = () => { }) const mutation = useMutation({ - mutationFn: (data: UserCreate) => - UsersService.createUser({ requestBody: data }), + mutationFn: (data: UserCreate) => UsersService.createUser({ body: data }), onSuccess: () => { showSuccessToast("User created successfully") form.reset() diff --git a/frontend/src/components/Admin/DeleteUser.tsx b/frontend/src/components/Admin/DeleteUser.tsx index 4ffd023e..48eca0a4 100644 --- a/frontend/src/components/Admin/DeleteUser.tsx +++ b/frontend/src/components/Admin/DeleteUser.tsx @@ -31,7 +31,7 @@ const DeleteUser = ({ id, onSuccess }: DeleteUserProps) => { const { handleSubmit } = useForm() const deleteUser = async (id: string) => { - await UsersService.deleteUser({ userId: id }) + await UsersService.deleteUser({ path: { user_id: id } }) } const mutation = useMutation({ diff --git a/frontend/src/components/Admin/EditUser.tsx b/frontend/src/components/Admin/EditUser.tsx index 172904f6..9d001a2a 100644 --- a/frontend/src/components/Admin/EditUser.tsx +++ b/frontend/src/components/Admin/EditUser.tsx @@ -75,7 +75,7 @@ const EditUser = ({ user, onSuccess }: EditUserProps) => { const mutation = useMutation({ mutationFn: (data: FormData) => - UsersService.updateUser({ userId: user.id, requestBody: data }), + UsersService.updateUser({ path: { user_id: user.id }, body: data }), onSuccess: () => { showSuccessToast("User updated successfully") setIsOpen(false) diff --git a/frontend/src/components/Items/AddItem.tsx b/frontend/src/components/Items/AddItem.tsx index 7c7c10cf..717948f1 100644 --- a/frontend/src/components/Items/AddItem.tsx +++ b/frontend/src/components/Items/AddItem.tsx @@ -53,8 +53,7 @@ const AddItem = () => { }) const mutation = useMutation({ - mutationFn: (data: ItemCreate) => - ItemsService.createItem({ requestBody: data }), + mutationFn: (data: ItemCreate) => ItemsService.createItem({ body: data }), onSuccess: () => { showSuccessToast("Item created successfully") form.reset() diff --git a/frontend/src/components/Items/DeleteItem.tsx b/frontend/src/components/Items/DeleteItem.tsx index 9e61c348..cbefb39f 100644 --- a/frontend/src/components/Items/DeleteItem.tsx +++ b/frontend/src/components/Items/DeleteItem.tsx @@ -31,7 +31,7 @@ const DeleteItem = ({ id, onSuccess }: DeleteItemProps) => { const { handleSubmit } = useForm() const deleteItem = async (id: string) => { - await ItemsService.deleteItem({ id: id }) + await ItemsService.deleteItem({ path: { id } }) } const mutation = useMutation({ diff --git a/frontend/src/components/Items/EditItem.tsx b/frontend/src/components/Items/EditItem.tsx index 3d57f559..9fa043d7 100644 --- a/frontend/src/components/Items/EditItem.tsx +++ b/frontend/src/components/Items/EditItem.tsx @@ -59,7 +59,7 @@ const EditItem = ({ item, onSuccess }: EditItemProps) => { const mutation = useMutation({ mutationFn: (data: FormData) => - ItemsService.updateItem({ id: item.id, requestBody: data }), + ItemsService.updateItem({ path: { id: item.id }, body: data }), onSuccess: () => { showSuccessToast("Item updated successfully") setIsOpen(false) diff --git a/frontend/src/components/UserSettings/ChangePassword.tsx b/frontend/src/components/UserSettings/ChangePassword.tsx index aeb85370..c2784d37 100644 --- a/frontend/src/components/UserSettings/ChangePassword.tsx +++ b/frontend/src/components/UserSettings/ChangePassword.tsx @@ -53,7 +53,7 @@ const ChangePassword = () => { const mutation = useMutation({ mutationFn: (data: UpdatePassword) => - UsersService.updatePasswordMe({ requestBody: data }), + UsersService.updatePasswordMe({ body: data }), onSuccess: () => { showSuccessToast("Password updated successfully") form.reset() diff --git a/frontend/src/components/UserSettings/UserInformation.tsx b/frontend/src/components/UserSettings/UserInformation.tsx index 4bfaf600..93a65aa4 100644 --- a/frontend/src/components/UserSettings/UserInformation.tsx +++ b/frontend/src/components/UserSettings/UserInformation.tsx @@ -50,7 +50,7 @@ const UserInformation = () => { const mutation = useMutation({ mutationFn: (data: UserUpdateMe) => - UsersService.updateUserMe({ requestBody: data }), + UsersService.updateUserMe({ body: data }), onSuccess: () => { showSuccessToast("User updated successfully") toggleEditMode() diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 7ccc795c..051b222a 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -22,13 +22,13 @@ const useAuth = () => { const { data: user } = useQuery({ queryKey: ["currentUser"], - queryFn: UsersService.readUserMe, + queryFn: async () => (await UsersService.readUserMe()).data, enabled: isLoggedIn(), }) const signUpMutation = useMutation({ mutationFn: (data: UserRegister) => - UsersService.registerUser({ requestBody: data }), + UsersService.registerUser({ body: data }), onSuccess: () => { navigate({ to: "/login" }) }, @@ -40,9 +40,9 @@ const useAuth = () => { const login = async (data: AccessToken) => { const response = await LoginService.loginAccessToken({ - formData: data, + body: data, }) - localStorage.setItem("access_token", response.access_token) + localStorage.setItem("access_token", response.data.access_token) } const loginMutation = useMutation({ diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f9dcb0dc..f8275a34 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,21 +5,25 @@ import { QueryClientProvider, } from "@tanstack/react-query" import { createRouter, RouterProvider } from "@tanstack/react-router" +import { AxiosError } from "axios" import { StrictMode } from "react" import ReactDOM from "react-dom/client" -import { ApiError, OpenAPI } from "./client" +import { client } from "./client/client.gen" import { ThemeProvider } from "./components/theme-provider" import { Toaster } from "./components/ui/sonner" import "./index.css" import { routeTree } from "./routeTree.gen" -OpenAPI.BASE = import.meta.env.VITE_API_URL ?? "" -OpenAPI.TOKEN = async () => { - return localStorage.getItem("access_token") || "" -} +client.setConfig({ + baseURL: import.meta.env.VITE_API_URL ?? "", + auth: () => localStorage.getItem("access_token") || "", +}) const handleApiError = (error: Error) => { - if (error instanceof ApiError && [401, 403].includes(error.status)) { + if ( + error instanceof AxiosError && + [401, 403].includes(error.response?.status ?? 0) + ) { localStorage.removeItem("access_token") window.location.href = "/login" } diff --git a/frontend/src/routes/_layout/admin.tsx b/frontend/src/routes/_layout/admin.tsx index a53ff2c4..39241c23 100644 --- a/frontend/src/routes/_layout/admin.tsx +++ b/frontend/src/routes/_layout/admin.tsx @@ -11,7 +11,8 @@ import useAuth from "@/hooks/useAuth" function getUsersQueryOptions() { return { - queryFn: () => UsersService.readUsers({ skip: 0, limit: 100 }), + queryFn: async () => + (await UsersService.readUsers({ query: { skip: 0, limit: 100 } })).data, queryKey: ["users"], } } @@ -19,7 +20,7 @@ function getUsersQueryOptions() { export const Route = createFileRoute("/_layout/admin")({ component: Admin, beforeLoad: async () => { - const user = await UsersService.readUserMe() + const { data: user } = await UsersService.readUserMe() if (!user.is_superuser) { throw redirect({ to: "/", diff --git a/frontend/src/routes/_layout/items.tsx b/frontend/src/routes/_layout/items.tsx index a4df2000..0dbb97f9 100644 --- a/frontend/src/routes/_layout/items.tsx +++ b/frontend/src/routes/_layout/items.tsx @@ -11,7 +11,8 @@ import PendingItems from "@/components/Pending/PendingItems" function getItemsQueryOptions() { return { - queryFn: () => ItemsService.readItems({ skip: 0, limit: 100 }), + queryFn: async () => + (await ItemsService.readItems({ query: { skip: 0, limit: 100 } })).data, queryKey: ["items"], } } diff --git a/frontend/src/routes/recover-password.tsx b/frontend/src/routes/recover-password.tsx index 058765df..38118ee4 100644 --- a/frontend/src/routes/recover-password.tsx +++ b/frontend/src/routes/recover-password.tsx @@ -59,7 +59,7 @@ function RecoverPassword() { const recoverPassword = async (data: FormData) => { await LoginService.recoverPassword({ - email: data.email, + path: { email: data.email }, }) } diff --git a/frontend/src/routes/reset-password.tsx b/frontend/src/routes/reset-password.tsx index b9d5562a..bbbf3eef 100644 --- a/frontend/src/routes/reset-password.tsx +++ b/frontend/src/routes/reset-password.tsx @@ -83,7 +83,7 @@ function ResetPassword() { const mutation = useMutation({ mutationFn: (data: { new_password: string; token: string }) => - LoginService.resetPassword({ requestBody: data }), + LoginService.resetPassword({ body: data }), onSuccess: () => { showSuccessToast("Password updated successfully") form.reset() diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index fa491eb2..967bb471 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -1,22 +1,20 @@ import { AxiosError } from "axios" -import type { ApiError } from "./client" -function extractErrorMessage(err: ApiError): string { +function extractErrorMessage(err: Error): string { if (err instanceof AxiosError) { + const errDetail = (err.response?.data as any)?.detail + if (Array.isArray(errDetail) && errDetail.length > 0) { + return errDetail[0].msg + } + if (typeof errDetail === "string") { + return errDetail + } return err.message } - - const errDetail = (err.body as any)?.detail - if (Array.isArray(errDetail) && errDetail.length > 0) { - return errDetail[0].msg - } - return errDetail || "Something went wrong." + return "Something went wrong." } -export const handleError = function ( - this: (msg: string) => void, - err: ApiError, -) { +export const handleError = function (this: (msg: string) => void, err: Error) { const errorMessage = extractErrorMessage(err) this(errorMessage) } diff --git a/frontend/tests/utils/privateApi.ts b/frontend/tests/utils/privateApi.ts index b6fa0afe..fdbcacbd 100644 --- a/frontend/tests/utils/privateApi.ts +++ b/frontend/tests/utils/privateApi.ts @@ -1,8 +1,11 @@ // Note: the `PrivateService` is only available when generating the client // for local environments -import { OpenAPI, PrivateService } from "../../src/client" +import { PrivateService } from "../../src/client" +import { client } from "../../src/client/client.gen" -OpenAPI.BASE = `${process.env.VITE_API_URL}` +client.setConfig({ + baseURL: `${process.env.VITE_API_URL}`, +}) export const createUser = async ({ email, @@ -11,12 +14,13 @@ export const createUser = async ({ email: string password: string }) => { - return await PrivateService.createUser({ - requestBody: { + const response = await PrivateService.createUser({ + body: { email, password, is_verified: true, full_name: "Test User", }, }) + return response.data } From d506ea4883c0f7bfcf5280921cfc407c46808711 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:25:31 +0000 Subject: [PATCH 12/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index aed3b375..18ca3311 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.1.13 to >=3.3.4,<4.0.0. PR [#2399](https://github.com/fastapi/full-stack-fastapi-template/pull/2399) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the github-actions group with 5 updates. PR [#2397](https://github.com/fastapi/full-stack-fastapi-template/pull/2397) by [@dependabot[bot]](https://github.com/apps/dependabot). From 96c1147ee7eb33b84c0942585011f57b5a0ba7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 8 Aug 2026 18:07:39 +0200 Subject: [PATCH 13/91] =?UTF-8?q?=F0=9F=94=92=20Replace=20pre-commit=20PAT?= =?UTF-8?q?=20with=20PR=20Push=20(#2407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pr-push.yml | 2 ++ .github/workflows/pre-commit.yml | 36 ++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .github/pr-push.yml diff --git a/.github/pr-push.yml b/.github/pr-push.yml new file mode 100644 index 00000000..15913650 --- /dev/null +++ b/.github/pr-push.yml @@ -0,0 +1,2 @@ +workflows: + - .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 98c50d66..2dc07362 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -2,16 +2,17 @@ name: pre-commit on: pull_request: -permissions: - contents: read +permissions: {} env: - # Forks and Dependabot don't have access to secrets - HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }} + CAN_PUSH: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} jobs: pre-commit: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write timeout-minutes: 5 steps: - name: Dump GitHub context @@ -20,20 +21,18 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout PR for own repo - if: env.HAS_SECRETS == 'true' + if: env.CAN_PUSH == 'true' with: # To be able to commit it needs to fetch the head of the branch, not the # merge commit ref: ${{ github.head_ref }} # And it needs the full history to be able to compute diffs fetch-depth: 0 - # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI - token: ${{ secrets.PRE_COMMIT }} # zizmor: ignore[secrets-outside-env] - persist-credentials: true # Required for `git push` command + persist-credentials: false # pre-commit lite ci needs the default checkout configs to work - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout PR for fork - if: env.HAS_SECRETS == 'false' + if: env.CAN_PUSH == 'false' with: # To be able to commit it needs the head branch of the PR, the remote one ref: ${{ github.event.pull_request.head.sha }} @@ -64,11 +63,26 @@ jobs: id: precommit run: uv run prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure continue-on-error: true + - name: Check for changes + id: changes + run: | + if [[ -n "$(git status --porcelain)" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + - name: Get PR Push token + id: pr-push + if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' + uses: tiangolo/pr-push@ff4e51a433de4c22bbf90597e069e8247b9203d2 # 0.0.1 - name: Commit and push changes - if: env.HAS_SECRETS == 'true' + if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' + env: + PR_PUSH_TOKEN: ${{ steps.pr-push.outputs.token }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${PR_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" git add -A if git diff --staged --quiet; then echo "No changes to commit" @@ -77,7 +91,7 @@ jobs: git push fi - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - if: env.HAS_SECRETS == 'false' + if: env.CAN_PUSH == 'false' with: msg: 🎨 Auto format and update with pre-commit - name: Error out on pre-commit errors From e5cdc73405042be59751b0adba0c26a181309a7b Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:07:42 +0000 Subject: [PATCH 14/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 18ca3311..445799bb 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.1.13 to >=3.3.4,<4.0.0. PR [#2399](https://github.com/fastapi/full-stack-fastapi-template/pull/2399) by [@dependabot[bot]](https://github.com/apps/dependabot). From 34378100a8f6020120ecf81cac8214232e7d5af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 8 Aug 2026 23:23:30 +0200 Subject: [PATCH 15/91] =?UTF-8?q?=F0=9F=91=B7=20Use=20PR=20Push=20commit?= =?UTF-8?q?=20identity=20(#2409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 2dc07362..9d53bef4 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -80,8 +80,8 @@ jobs: env: PR_PUSH_TOKEN: ${{ steps.pr-push.outputs.token }} run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "pr-push[bot]" + git config user.email "pr-push[bot]@users.noreply.github.com" git remote set-url origin "https://x-access-token:${PR_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" git add -A if git diff --staged --quiet; then From 220bcb74aabd294bc388134360c7758d032c9fbb Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:23:32 +0000 Subject: [PATCH 16/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 445799bb..b433f221 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo). * 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo). From 6a1c084f6ec9ddf52fc0608af7b1d79dde1bebab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 00:28:17 +0200 Subject: [PATCH 17/91] =?UTF-8?q?=F0=9F=91=B7=20Use=20GitHub=20CLI=20for?= =?UTF-8?q?=20Git=20authentication=20(#2410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9d53bef4..a1ea6d2c 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -78,11 +78,11 @@ jobs: - name: Commit and push changes if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' env: - PR_PUSH_TOKEN: ${{ steps.pr-push.outputs.token }} + GH_TOKEN: ${{ steps.pr-push.outputs.token }} run: | git config user.name "pr-push[bot]" git config user.email "pr-push[bot]@users.noreply.github.com" - git remote set-url origin "https://x-access-token:${PR_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + gh auth setup-git git add -A if git diff --staged --quiet; then echo "No changes to commit" From 9097b329d696c071e58647f409d081bcced75a1b Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:28:20 +0000 Subject: [PATCH 18/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index b433f221..2b257e7d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 👷 Use GitHub CLI for Git authentication. PR [#2410](https://github.com/fastapi/full-stack-fastapi-template/pull/2410) by [@tiangolo](https://github.com/tiangolo). * 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo). * 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). From d5b20172e401ed63ea7a4dae6a09a2ed4936a464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 00:47:39 +0200 Subject: [PATCH 19/91] =?UTF-8?q?=F0=9F=94=90=20Use=20PR=20Submit=20for=20?= =?UTF-8?q?pull=20requests=20(#2411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pr-submit.yml | 2 ++ .github/workflows/bump-pre-commit-hooks.yml | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .github/pr-submit.yml diff --git a/.github/pr-submit.yml b/.github/pr-submit.yml new file mode 100644 index 00000000..1c5d0aa4 --- /dev/null +++ b/.github/pr-submit.yml @@ -0,0 +1,2 @@ +workflows: + - .github/workflows/bump-pre-commit-hooks.yml diff --git a/.github/workflows/bump-pre-commit-hooks.yml b/.github/workflows/bump-pre-commit-hooks.yml index 9442b353..c549b207 100644 --- a/.github/workflows/bump-pre-commit-hooks.yml +++ b/.github/workflows/bump-pre-commit-hooks.yml @@ -12,6 +12,9 @@ jobs: if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + id-token: write steps: - name: Dump GitHub context env: @@ -19,8 +22,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - token: ${{ secrets.LATEST_CHANGES }} - persist-credentials: true + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: @@ -36,9 +38,12 @@ jobs: uv.lock - name: Bump pre-commit hooks run: uv run prek auto-update --freeze --cooldown-days 7 + - name: Get PR Submit token + id: pr-submit + uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1 - name: Create pull request env: - GH_TOKEN: ${{ secrets.LATEST_CHANGES }} + GH_TOKEN: ${{ steps.pr-submit.outputs.token }} BASE_BRANCH: ${{ github.event.repository.default_branch }} run: | set -euo pipefail @@ -46,12 +51,13 @@ jobs: echo "No pre-commit hook updates available" exit 0 fi - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "pr-submit[bot]" + git config user.email "pr-submit[bot]@users.noreply.github.com" branch="bump-pre-commit-hooks" git switch -C "$branch" git add .pre-commit-config.yaml git commit -m "⬆ Bump pre-commit hooks" + gh auth setup-git git push --force origin "$branch" if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then gh pr create \ From 75c388ba1cd60f39b43dd63a73978bae8c69cb87 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:47:42 +0000 Subject: [PATCH 20/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 2b257e7d..f9902d91 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 🔐 Use PR Submit for pull requests. PR [#2411](https://github.com/fastapi/full-stack-fastapi-template/pull/2411) by [@tiangolo](https://github.com/tiangolo). * 👷 Use GitHub CLI for Git authentication. PR [#2410](https://github.com/fastapi/full-stack-fastapi-template/pull/2410) by [@tiangolo](https://github.com/tiangolo). * 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo). * 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). From 0137c3f2c34a15fe825bc0f980d034ed946e2d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 20:21:42 +0200 Subject: [PATCH 21/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20Traefik?= =?UTF-8?q?=20deployment,=20make=20it=20part=20of=20the=20stack=20(#2412)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 - .github/workflows/deploy-production.yml | 9 +- .github/workflows/deploy-staging.yml | 9 +- README.md | 3 +- compose.deploy.yml | 59 ++++++++++++ compose.override.yml | 25 ----- compose.traefik.yml | 77 ---------------- compose.yml | 68 ++++++-------- copier.yml | 5 - deployment.md | 116 ++---------------------- development.md | 4 +- 11 files changed, 109 insertions(+), 267 deletions(-) create mode 100644 compose.deploy.yml delete mode 100644 compose.traefik.yml diff --git a/.env b/.env index f0887ce4..2ab3e7e9 100644 --- a/.env +++ b/.env @@ -15,7 +15,6 @@ FRONTEND_HOST=http://localhost:5173 ENVIRONMENT=local PROJECT_NAME="Full Stack FastAPI Project" -STACK_NAME=full-stack-fastapi-project # Backend BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://localhost.tiangolo.com" diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index f86c79e6..9c2b3965 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -18,9 +18,8 @@ jobs: - production env: ENVIRONMENT: production - DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }} - FRONTEND_HOST: https://${{ secrets.DOMAIN_PRODUCTION }} - STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }} + DOMAIN: ${{ secrets.DOMAIN }} + FRONTEND_HOST: https://${{ secrets.DOMAIN }} SECRET_KEY: ${{ secrets.SECRET_KEY }} FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} @@ -35,5 +34,5 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build - - run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d + - run: docker compose -f compose.yml -f compose.deploy.yml build + - run: docker compose -f compose.yml -f compose.deploy.yml up -d diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 21adf429..61c3dd96 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -18,9 +18,8 @@ jobs: - staging env: ENVIRONMENT: staging - DOMAIN: ${{ secrets.DOMAIN_STAGING }} - FRONTEND_HOST: https://${{ secrets.DOMAIN_STAGING }} - STACK_NAME: ${{ secrets.STACK_NAME_STAGING }} + DOMAIN: ${{ secrets.DOMAIN }} + FRONTEND_HOST: https://${{ secrets.DOMAIN }} SECRET_KEY: ${{ secrets.SECRET_KEY }} FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} @@ -35,5 +34,5 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build - - run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d + - run: docker compose -f compose.yml -f compose.deploy.yml build + - run: docker compose -f compose.yml -f compose.deploy.yml up -d diff --git a/README.md b/README.md index 4cfc3446..57b170b3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development. - ✅ Tests with [Pytest](https://pytest.org). - 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer. -- 🚢 Deployment instructions using Docker Compose, including how to set up Traefik to handle automatic HTTPS certificates. +- 🚢 Deployment instructions using Docker Compose with automatic HTTPS provided by Traefik. - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. ### Dashboard Login @@ -196,7 +196,6 @@ But don't worry, you can just update any of that in the `.env` files afterwards. The input variables, with their default values (some auto generated) are: - `project_name`: (default: `"FastAPI Project"`) The name of the project, shown to API users (in .env). -- `stack_name`: (default: `"fastapi-project"`) The name of the stack used for Docker Compose labels and project name (no spaces, no periods) (in .env). - `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above. - `first_superuser`: (default: `"admin@example.com"`) The email of the first superuser (in .env). - `first_superuser_password`: (default: `"changethis"`) The password of the first superuser (in .env). diff --git a/compose.deploy.yml b/compose.deploy.yml new file mode 100644 index 00000000..5fa51263 --- /dev/null +++ b/compose.deploy.yml @@ -0,0 +1,59 @@ +services: + + proxy: + restart: always + ports: + # Listen on port 80, default for HTTP, necessary to redirect to HTTPS + - "80:80" + # Listen on port 443, default for HTTPS + - "443:443" + volumes: + # Mount the volume to store the certificates + - traefik-certificates:/certificates + command: + # Enable Docker in Traefik, so that it reads labels from Docker services + - --providers.docker + # Do not expose all Docker services, only the ones explicitly exposed + - --providers.docker.exposedbydefault=false + # Create an entrypoint "http" listening on port 80 + - --entrypoints.http.address=:80 + # Redirect all HTTP traffic to HTTPS + - --entrypoints.http.http.redirections.entrypoint.to=https + - --entrypoints.http.http.redirections.entrypoint.scheme=https + # Create an entrypoint "https" listening on port 443 + - --entrypoints.https.address=:443 + # Store the Let's Encrypt certificates in the mounted volume + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + # Use the TLS Challenge for Let's Encrypt + - --certificatesresolvers.le.acme.tlschallenge=true + # Enable the access log, with HTTP requests + - --accesslog + # Enable the Traefik log, for configurations and errors + - --log + + db: + restart: always + + adminer: + restart: always + labels: + # Route HTTPS traffic for the Adminer subdomain + - traefik.http.routers.adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`) + - traefik.http.routers.adminer-https.entrypoints=https + - traefik.http.routers.adminer-https.tls=true + # Use the Let's Encrypt resolver + - traefik.http.routers.adminer-https.tls.certresolver=le + + backend: + restart: always + labels: + # Route HTTPS traffic for this domain + - traefik.http.routers.backend-https.rule=Host(`${DOMAIN?Variable not set}`) + - traefik.http.routers.backend-https.entrypoints=https + - traefik.http.routers.backend-https.tls=true + # Use the Let's Encrypt resolver + - traefik.http.routers.backend-https.tls.certresolver=le + +volumes: + # Create a volume to store the certificates, even if the container is recreated + traefik-certificates: diff --git a/compose.override.yml b/compose.override.yml index 0c9e77dc..2378d468 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -6,8 +6,6 @@ services: # DOMAIN=localhost.tiangolo.com proxy: image: traefik:3.6 - volumes: - - /var/run/docker.sock:/var/run/docker.sock ports: - "80:80" - "8090:8080" @@ -15,14 +13,10 @@ services: command: # Enable Docker in Traefik, so that it reads labels from Docker services - --providers.docker - # Add a constraint to only use services with the label for this stack - - --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`) # Do not expose all Docker services, only the ones explicitly exposed - --providers.docker.exposedbydefault=false # Create an entrypoint "http" listening on port 80 - --entrypoints.http.address=:80 - # Create an entrypoint "https" listening on port 443 - - --entrypoints.https.address=:443 # Enable the access log, with HTTP requests - --accesslog # Enable the Traefik log, for configurations and errors @@ -33,29 +27,15 @@ services: - --api # Enable the Dashboard and API in insecure mode for local development - --api.insecure=true - labels: - # Enable Traefik for this service, to make it available in the public network - - traefik.enable=true - - traefik.constraint-label=traefik-public - # Dummy https-redirect middleware that doesn't really redirect, only to - # allow running it locally - - traefik.http.middlewares.https-redirect.contenttype.autodetect=false - networks: - - traefik-public - - default - db: - restart: "no" ports: - "5432:5432" adminer: - restart: "no" ports: - "8080:8080" backend: - restart: "no" ports: - "8000:8000" build: @@ -123,8 +103,3 @@ services: - ./frontend/test-results:/app/frontend/test-results ports: - 9323:9323 - -networks: - traefik-public: - # For local dev, don't expect an external Traefik network - external: false diff --git a/compose.traefik.yml b/compose.traefik.yml deleted file mode 100644 index bcd7d142..00000000 --- a/compose.traefik.yml +++ /dev/null @@ -1,77 +0,0 @@ -services: - traefik: - image: traefik:3.6 - ports: - # Listen on port 80, default for HTTP, necessary to redirect to HTTPS - - 80:80 - # Listen on port 443, default for HTTPS - - 443:443 - restart: always - labels: - # Enable Traefik for this service, to make it available in the public network - - traefik.enable=true - # Use the traefik-public network (declared below) - - traefik.docker.network=traefik-public - # Define the port inside of the Docker service to use - - traefik.http.services.traefik-dashboard.loadbalancer.server.port=8080 - # Make Traefik use this domain (from an environment variable) in HTTP - - traefik.http.routers.traefik-dashboard-http.entrypoints=http - - traefik.http.routers.traefik-dashboard-http.rule=Host(`traefik.${DOMAIN?Variable not set}`) - # traefik-https the actual router using HTTPS - - traefik.http.routers.traefik-dashboard-https.entrypoints=https - - traefik.http.routers.traefik-dashboard-https.rule=Host(`traefik.${DOMAIN?Variable not set}`) - - traefik.http.routers.traefik-dashboard-https.tls=true - # Use the "le" (Let's Encrypt) resolver created below - - traefik.http.routers.traefik-dashboard-https.tls.certresolver=le - # Use the special Traefik service api@internal with the web UI/Dashboard - - traefik.http.routers.traefik-dashboard-https.service=api@internal - # https-redirect middleware to redirect HTTP to HTTPS - - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https - - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true - # traefik-http set up only to use the middleware to redirect to https - - traefik.http.routers.traefik-dashboard-http.middlewares=https-redirect - # admin-auth middleware with HTTP Basic auth - # Using the environment variables USERNAME and HASHED_PASSWORD - - traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set} - # Enable HTTP Basic auth, using the middleware created above - - traefik.http.routers.traefik-dashboard-https.middlewares=admin-auth - volumes: - # Add Docker as a mounted volume, so that Traefik can read the labels of other services - - /var/run/docker.sock:/var/run/docker.sock:ro - # Mount the volume to store the certificates - - traefik-public-certificates:/certificates - command: - # Enable Docker in Traefik, so that it reads labels from Docker services - - --providers.docker - # Do not expose all Docker services, only the ones explicitly exposed - - --providers.docker.exposedbydefault=false - # Create an entrypoint "http" listening on port 80 - - --entrypoints.http.address=:80 - # Create an entrypoint "https" listening on port 443 - - --entrypoints.https.address=:443 - # Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL - - --certificatesresolvers.le.acme.email=${EMAIL?Variable not set} - # Store the Let's Encrypt certificates in the mounted volume - - --certificatesresolvers.le.acme.storage=/certificates/acme.json - # Use the TLS Challenge for Let's Encrypt - - --certificatesresolvers.le.acme.tlschallenge=true - # Enable the access log, with HTTP requests - - --accesslog - # Enable the Traefik log, for configurations and errors - - --log - # Enable the Dashboard and API - - --api - networks: - # Use the public network created to be shared between Traefik and - # any other service that needs to be publicly available with HTTPS - - traefik-public - -volumes: - # Create a volume to store the certificates, even if the container is recreated - traefik-public-certificates: - -networks: - # Use the previously created public network "traefik-public", shared with other - # services that need to be publicly available via this Traefik - traefik-public: - external: true diff --git a/compose.yml b/compose.yml index 6debf53c..ac7625d9 100644 --- a/compose.yml +++ b/compose.yml @@ -1,8 +1,24 @@ services: + proxy: + image: traefik:3.6 + volumes: + # Add Docker as a mounted volume, so that Traefik can read the labels of other services + - /var/run/docker.sock:/var/run/docker.sock:ro + command: + # Enable Docker in Traefik, so that it reads labels from Docker services + - --providers.docker + # Do not expose all Docker services, only the ones explicitly exposed + - --providers.docker.exposedbydefault=false + # Create an entrypoint "http" listening on port 80 + - --entrypoints.http.address=:80 + # Enable the access log, with HTTP requests + - --accesslog + # Enable the Traefik log, for configurations and errors + - --log + db: image: postgres:18 - restart: always healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s @@ -20,35 +36,24 @@ services: adminer: image: adminer - restart: always - networks: - - traefik-public - - default depends_on: - db environment: - ADMINER_DESIGN=pepa-linha-dark labels: + # Enable Traefik for this service - traefik.enable=true - - traefik.docker.network=traefik-public - - traefik.constraint-label=traefik-public - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.entrypoints=http - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.middlewares=https-redirect - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls.certresolver=le - - traefik.http.services.${STACK_NAME?Variable not set}-adminer.loadbalancer.server.port=8080 + # Route HTTP traffic for the Adminer subdomain + - traefik.http.routers.adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`) + - traefik.http.routers.adminer-http.entrypoints=http + # Define the port inside of the Docker service to use + - traefik.http.services.adminer.loadbalancer.server.port=8080 prestart: image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' build: context: . dockerfile: backend/Dockerfile - networks: - - traefik-public - - default depends_on: db: condition: service_healthy @@ -77,10 +82,6 @@ services: backend: image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' - restart: always - networks: - - traefik-public - - default depends_on: db: condition: service_healthy @@ -118,26 +119,15 @@ services: context: . dockerfile: backend/Dockerfile labels: + # Enable Traefik for this service - traefik.enable=true - - traefik.docker.network=traefik-public - - traefik.constraint-label=traefik-public - - traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000 + # Define the port inside of the Docker service to use + - traefik.http.services.backend.loadbalancer.server.port=8000 - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http + # Route HTTP traffic for this domain + - traefik.http.routers.backend-http.rule=Host(`${DOMAIN?Variable not set}`) + - traefik.http.routers.backend-http.entrypoints=http - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le - - # Enable redirection for HTTP and HTTPS - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect volumes: app-db-data: - -networks: - traefik-public: - # Allow setting it to false for testing - external: true diff --git a/copier.yml b/copier.yml index f98e3fc8..f28369e8 100644 --- a/copier.yml +++ b/copier.yml @@ -3,11 +3,6 @@ project_name: help: The name of the project, shown to API users (in .env) default: FastAPI Project -stack_name: - type: str - help: The name of the stack used for Docker Compose labels (no spaces) (in .env) - default: fastapi-project - secret_key: type: str help: | diff --git a/deployment.md b/deployment.md index cc470e58..32bd71e4 100644 --- a/deployment.md +++ b/deployment.md @@ -2,7 +2,7 @@ You can deploy the project using Docker Compose to a remote server. -This project expects you to have a Traefik proxy handling communication to the outside world and HTTPS certificates. +The deployment Docker Compose configuration includes Traefik to handle HTTPS and route incoming traffic to the application. You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions. @@ -10,101 +10,13 @@ But you have to configure a couple things first. 🤓 ## Preparation -* Have a remote server ready and available. -* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com`, `traefik.fastapi-project.example.com`, and `adminer.fastapi-project.example.com`. Do the same for staging domains such as `staging.fastapi-project.example.com`. +* Have a remote server ready and available. Use a separate server for each environment, for example one for staging and one for production. +* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com` and `adminer.fastapi-project.example.com`. * Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop). -## Public Traefik - -We need a Traefik proxy to handle incoming connections and HTTPS certificates. - -You need to do these next steps only once. - -### Traefik Docker Compose - -* Create a remote directory to store your Traefik Docker Compose file: - -```bash -mkdir -p /root/code/traefik-public/ -``` - -Copy the Traefik Docker Compose file to your server. You could do it by running the command `rsync` in your local terminal: - -```bash -rsync -a compose.traefik.yml root@your-server.example.com:/root/code/traefik-public/ -``` - -### Traefik Public Network - -This Traefik will expect a Docker "public network" named `traefik-public` to communicate with your stack(s). - -This way, there will be a single public Traefik proxy that handles the communication (HTTP and HTTPS) with the outside world, and then behind that, you could have one or more stacks with different domains, even if they are on the same single server. - -To create a Docker "public network" named `traefik-public` run the following command in your remote server: - -```bash -docker network create traefik-public -``` - -### Traefik Environment Variables - -The Traefik Docker Compose file expects some environment variables to be set in your terminal before starting it. You can do it by running the following commands in your remote server. - -* Create the username for HTTP Basic Auth, e.g.: - -```bash -export USERNAME=admin -``` - -* Create an environment variable with the password for HTTP Basic Auth, e.g.: - -```bash -export PASSWORD=changethis -``` - -* Use openssl to generate the "hashed" version of the password for HTTP Basic Auth and store it in an environment variable: - -```bash -export HASHED_PASSWORD=$(openssl passwd -apr1 $PASSWORD) -``` - -To verify that the hashed password is correct, you can print it: - -```bash -echo $HASHED_PASSWORD -``` - -* Create an environment variable with the domain name for your server, e.g.: - -```bash -export DOMAIN=fastapi-project.example.com -``` - -* Create an environment variable with the email for Let's Encrypt, e.g.: - -```bash -export EMAIL=admin@example.com -``` - -**Note**: you need to set a different email, an email `@example.com` won't work. - -### Start the Traefik Docker Compose - -Go to the directory where you copied the Traefik Docker Compose file in your remote server: - -```bash -cd /root/code/traefik-public/ -``` - -Now with the environment variables set and the `compose.traefik.yml` in place, you can start the Traefik Docker Compose running the following command: - -```bash -docker compose -f compose.traefik.yml up -d -``` - ## Deploy the FastAPI Project -Now that you have Traefik in place you can deploy your FastAPI project with Docker Compose. +You can deploy your FastAPI project with Docker Compose. **Note**: You might want to jump ahead to the section about Continuous Deployment with GitHub Actions. @@ -175,7 +87,6 @@ export FRONTEND_HOST="https://${DOMAIN?Variable not set}" You can set several other environment variables: * `PROJECT_NAME`: The name of the project, used in the API for the docs and emails. -* `STACK_NAME`: The name of the stack used for Docker Compose labels and project name, this should be different for `staging`, `production`, etc. You could use the same domain replacing dots with dashes, e.g. `fastapi-project-example-com` and `staging-fastapi-project-example-com`. * `BACKEND_CORS_ORIGINS`: A list of additional allowed CORS origins separated by commas. The frontend served by FastAPI uses the same origin and doesn't need to be added. * `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users. * `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc). @@ -201,19 +112,17 @@ With the environment variables in place, you can deploy with Docker Compose: ```bash cd /root/code/app/ -docker compose -f compose.yml build -docker compose -f compose.yml up -d +docker compose -f compose.yml -f compose.deploy.yml build +docker compose -f compose.yml -f compose.deploy.yml up -d ``` -For production you wouldn't want to have the overrides in `compose.override.yml`, that's why we explicitly specify `compose.yml` as the file to use. +The `compose.deploy.yml` file adds the deployment settings to the shared configuration in `compose.yml`, including HTTPS and automatic certificate handling. Explicitly listing these files also excludes the local development settings in `compose.override.yml`. ## Continuous Deployment (CD) You can use GitHub Actions to deploy your project automatically. 😎 -You can have multiple environment deployments. - -There are already two environments configured, `staging` and `production`. 🚀 +There are already two environment deployments configured, `staging` and `production`. Each environment should be deployed to a separate server. 🚀 ### Install GitHub Actions Runner @@ -299,10 +208,7 @@ For each GitHub Environment (`staging` and `production`), configure the required The current Github Actions workflows expect these secrets: -* `DOMAIN_PRODUCTION` -* `DOMAIN_STAGING` -* `STACK_NAME_PRODUCTION` -* `STACK_NAME_STAGING` +* `DOMAIN` * `EMAILS_FROM_EMAIL` * `FIRST_SUPERUSER` * `FIRST_SUPERUSER_PASSWORD` @@ -326,10 +232,6 @@ If you need to add extra environments you could use those as a starting point. Replace `fastapi-project.example.com` with your domain. -### Main Traefik Dashboard - -Traefik UI: `https://traefik.fastapi-project.example.com` - ### Production Application (frontend and API): `https://fastapi-project.example.com` diff --git a/development.md b/development.md index 250c1db8..576d3930 100644 --- a/development.md +++ b/development.md @@ -97,7 +97,7 @@ After you update it, run again: docker compose watch ``` -When deploying, for example in production, the main Traefik is configured outside of the Docker Compose files. For local development, there's an included Traefik in `compose.override.yml`, just to let you test that the domain works as expected, for example with `localhost.tiangolo.com`. +Traefik is included in the main Docker Compose file. The development overrides expose its local dashboard so you can inspect the routes used with `localhost.tiangolo.com`. ## Docker Compose files and env vars @@ -105,6 +105,8 @@ There is a main `compose.yml` file with all the configurations that apply to the And there's also a `compose.override.yml` with overrides for development, for example to mount the source code as a volume. It is used automatically by `docker compose` to apply overrides on top of `compose.yml`. +The `compose.deploy.yml` file contains the deployment-specific settings, including HTTPS and automatic certificate handling. It is explicitly combined with `compose.yml` when deploying the application. + These Docker Compose files use the `.env` file containing configurations to be injected as environment variables in the containers. They also use some additional configurations taken from environment variables set in the scripts before calling the `docker compose` command. From 2d04828d6ab7c32699d0825bc5af00370d9a9f48 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:21:44 +0000 Subject: [PATCH 22/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index f9902d91..aa103ce3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify Traefik deployment, make it part of the stack. PR [#2412](https://github.com/fastapi/full-stack-fastapi-template/pull/2412) by [@tiangolo](https://github.com/tiangolo). * ♻️ Serve frontend from FastAPI. PR [#2393](https://github.com/fastapi/full-stack-fastapi-template/pull/2393) by [@tiangolo](https://github.com/tiangolo). * 🔧 Refactor PostgreSQL env var config. PR [#2392](https://github.com/fastapi/full-stack-fastapi-template/pull/2392) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor models to improve types. PR [#2356](https://github.com/fastapi/full-stack-fastapi-template/pull/2356) by [@tiangolo](https://github.com/tiangolo). From 9c8983e2a5d4160b74e7b51f3015fbf19712443e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 22:26:19 +0200 Subject: [PATCH 23/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20environme?= =?UTF-8?q?nt=20configuration=20(#2413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 10 +-- .github/workflows/deploy-production.yml | 1 - .github/workflows/deploy-staging.yml | 1 - compose.deploy.yml | 6 ++ compose.yml | 87 ++++++++++--------------- deployment.md | 10 +-- 6 files changed, 47 insertions(+), 68 deletions(-) diff --git a/.env b/.env index 2ab3e7e9..efd4aa28 100644 --- a/.env +++ b/.env @@ -6,10 +6,8 @@ DOMAIN=localhost # DOMAIN=localhost.tiangolo.com # Public frontend URL, used by the backend to generate links in emails. -# Keep the Vite URL for local development; deployments use the application URL. +# Deployments derive this from DOMAIN in compose.deploy.yml. FRONTEND_HOST=http://localhost:5173 -# In staging and production, set this env var to the frontend host, e.g. -# FRONTEND_HOST=https://example.com # Environment: local, staging, production ENVIRONMENT=local @@ -17,7 +15,8 @@ ENVIRONMENT=local PROJECT_NAME="Full Stack FastAPI Project" # Backend -BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://localhost.tiangolo.com" +# Additional allowed CORS origins, separated by commas +BACKEND_CORS_ORIGINS= SECRET_KEY=changethis FIRST_SUPERUSER=admin@example.com FIRST_SUPERUSER_PASSWORD=changethis @@ -39,6 +38,3 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=changethis SENTRY_DSN= - -# Configure these with your own Docker registry images -DOCKER_IMAGE_BACKEND=backend diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 9c2b3965..cc4b34db 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -19,7 +19,6 @@ jobs: env: ENVIRONMENT: production DOMAIN: ${{ secrets.DOMAIN }} - FRONTEND_HOST: https://${{ secrets.DOMAIN }} SECRET_KEY: ${{ secrets.SECRET_KEY }} FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 61c3dd96..256bd0d2 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -19,7 +19,6 @@ jobs: env: ENVIRONMENT: staging DOMAIN: ${{ secrets.DOMAIN }} - FRONTEND_HOST: https://${{ secrets.DOMAIN }} SECRET_KEY: ${{ secrets.SECRET_KEY }} FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} diff --git a/compose.deploy.yml b/compose.deploy.yml index 5fa51263..a54c2d44 100644 --- a/compose.deploy.yml +++ b/compose.deploy.yml @@ -44,8 +44,14 @@ services: # Use the Let's Encrypt resolver - traefik.http.routers.adminer-https.tls.certresolver=le + prestart: + environment: + FRONTEND_HOST: https://${DOMAIN?Variable not set} + backend: restart: always + environment: + FRONTEND_HOST: https://${DOMAIN?Variable not set} labels: # Route HTTPS traffic for this domain - traefik.http.routers.backend-https.rule=Host(`${DOMAIN?Variable not set}`) diff --git a/compose.yml b/compose.yml index ac7625d9..bbc7aa65 100644 --- a/compose.yml +++ b/compose.yml @@ -49,65 +49,36 @@ services: # Define the port inside of the Docker service to use - traefik.http.services.adminer.loadbalancer.server.port=8080 - prestart: - image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' - build: - context: . - dockerfile: backend/Dockerfile - depends_on: - db: - condition: service_healthy - restart: true - command: bash scripts/prestart.sh - env_file: - - .env - environment: - - DOMAIN=${DOMAIN} - - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} - - ENVIRONMENT=${ENVIRONMENT} - - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} - - SECRET_KEY=${SECRET_KEY?Variable not set} - - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} - - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} - - SMTP_HOST=${SMTP_HOST} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} - - POSTGRES_SERVER=db - - POSTGRES_PORT=${POSTGRES_PORT} - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER?Variable not set} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - - SENTRY_DSN=${SENTRY_DSN} - backend: - image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' + image: backend:latest depends_on: db: condition: service_healthy restart: true prestart: condition: service_completed_successfully - env_file: - - .env - environment: - - DOMAIN=${DOMAIN} - - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} - - ENVIRONMENT=${ENVIRONMENT} - - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} - - SECRET_KEY=${SECRET_KEY?Variable not set} - - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} - - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} - - SMTP_HOST=${SMTP_HOST} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} - - POSTGRES_SERVER=db - - POSTGRES_PORT=${POSTGRES_PORT} - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER?Variable not set} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - - SENTRY_DSN=${SENTRY_DSN} + # The YAML anchor lets the prestart service reuse this environment mapping. + environment: &backend-environment + FRONTEND_HOST: ${FRONTEND_HOST?Variable not set} + ENVIRONMENT: ${ENVIRONMENT} + PROJECT_NAME: ${PROJECT_NAME?Variable not set} + BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS} + SECRET_KEY: ${SECRET_KEY?Variable not set} + FIRST_SUPERUSER: ${FIRST_SUPERUSER?Variable not set} + FIRST_SUPERUSER_PASSWORD: ${FIRST_SUPERUSER_PASSWORD?Variable not set} + SMTP_HOST: ${SMTP_HOST} + SMTP_USER: ${SMTP_USER} + SMTP_PASSWORD: ${SMTP_PASSWORD} + EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL} + SMTP_TLS: ${SMTP_TLS} + SMTP_SSL: ${SMTP_SSL} + SMTP_PORT: ${SMTP_PORT} + POSTGRES_SERVER: db + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER?Variable not set} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?Variable not set} + SENTRY_DSN: ${SENTRY_DSN} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"] @@ -129,5 +100,17 @@ services: - traefik.http.routers.backend-http.rule=Host(`${DOMAIN?Variable not set}`) - traefik.http.routers.backend-http.entrypoints=http + prestart: + image: backend:latest + build: + context: . + dockerfile: backend/Dockerfile + depends_on: + db: + condition: service_healthy + restart: true + command: bash scripts/prestart.sh + environment: *backend-environment + volumes: app-db-data: diff --git a/deployment.md b/deployment.md index 32bd71e4..1485c6ba 100644 --- a/deployment.md +++ b/deployment.md @@ -58,6 +58,8 @@ Set the `DOMAIN`, by default `localhost` (for development), but when deploying y export DOMAIN=fastapi-project.example.com ``` +The deployment Docker Compose configuration also uses `DOMAIN` to set the public frontend URL used in links generated by the backend. + Set the `POSTGRES_PASSWORD` to something different than `changethis`: ```bash @@ -72,18 +74,12 @@ export SECRET_KEY="changethis" Note: you can use the Python command above to generate a secure secret key. -Set the `FIRST_SUPER_USER_PASSWORD` to something different than `changethis`: +Set the `FIRST_SUPERUSER_PASSWORD` to something different than `changethis`: ```bash export FIRST_SUPERUSER_PASSWORD="changethis" ``` -Set the `FRONTEND_HOST` to the application URL. It is used to generate links in emails: - -```bash -export FRONTEND_HOST="https://${DOMAIN?Variable not set}" -``` - You can set several other environment variables: * `PROJECT_NAME`: The name of the project, used in the API for the docs and emails. From 66f444a63a11ce7b4b6df6c4fbe9e15b2fa7aa3a Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:26:21 +0000 Subject: [PATCH 24/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index aa103ce3..55714781 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify environment configuration. PR [#2413](https://github.com/fastapi/full-stack-fastapi-template/pull/2413) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify Traefik deployment, make it part of the stack. PR [#2412](https://github.com/fastapi/full-stack-fastapi-template/pull/2412) by [@tiangolo](https://github.com/tiangolo). * ♻️ Serve frontend from FastAPI. PR [#2393](https://github.com/fastapi/full-stack-fastapi-template/pull/2393) by [@tiangolo](https://github.com/tiangolo). * 🔧 Refactor PostgreSQL env var config. PR [#2392](https://github.com/fastapi/full-stack-fastapi-template/pull/2392) by [@tiangolo](https://github.com/tiangolo). From 8a213ddae738687ff06cbe1b1c20dc9832d29c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 17:23:47 +0200 Subject: [PATCH 25/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20local=20d?= =?UTF-8?q?evelopment=20workflow=20(#2414)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .env | 7 ++- .pre-commit-config.yaml | 2 +- README.md | 4 +- backend/README.md | 86 +++++++++++------------------- compose.override.yml | 1 - compose.yml | 3 -- development.md | 115 ++++++++++++++++++++++------------------ frontend/README.md | 16 ++---- 8 files changed, 103 insertions(+), 131 deletions(-) diff --git a/.env b/.env index efd4aa28..1226cb23 100644 --- a/.env +++ b/.env @@ -22,13 +22,12 @@ FIRST_SUPERUSER=admin@example.com FIRST_SUPERUSER_PASSWORD=changethis # Emails -SMTP_HOST= +SMTP_HOST=localhost SMTP_USER= SMTP_PASSWORD= EMAILS_FROM_EMAIL=info@example.com -SMTP_TLS=True -SMTP_SSL=False -SMTP_PORT=587 +SMTP_TLS=False +SMTP_PORT=1025 # Postgres POSTGRES_SERVER=localhost diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80bb540e..74c121c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,7 +64,7 @@ repos: entry: bash ./scripts/generate-client.sh pass_filenames: false language: unsupported - files: ^backend/.*$|^scripts/generate-client\.sh$ + files: ^backend/app/.*\.py$|^backend/pyproject\.toml$|^uv\.lock$|^frontend/openapi-ts\.config\.ts$|^scripts/generate-client\.sh$ - id: add-release-date language: unsupported diff --git a/README.md b/README.md index 57b170b3..9ea0f276 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ - 🤖 An automatically generated frontend client. - 🧪 [Playwright](https://playwright.dev) for End-to-End testing. - 🦇 Dark mode support. -- 🐋 [Docker Compose](https://www.docker.com) for development and production. +- 🐋 [Docker Compose](https://www.docker.com) for local services and deployment. - 🔒 Secure password hashing by default. - 🔑 JWT (JSON Web Token) authentication. - 📫 Email based password recovery. @@ -222,7 +222,7 @@ Deployment docs: [deployment.md](./deployment.md). General development docs: [development.md](./development.md). -This includes using Docker Compose, custom local domains, `.env` configurations, etc. +This includes the local FastAPI and Vite workflow, Docker Compose services, custom local domains, `.env` configuration, and more. ## Release Notes diff --git a/backend/README.md b/backend/README.md index 521ee3fa..d59c7a50 100644 --- a/backend/README.md +++ b/backend/README.md @@ -5,9 +5,25 @@ * [Docker](https://www.docker.com/). * [uv](https://docs.astral.sh/uv/) for Python package and environment management. -## Docker Compose +## Local Development -Start the local development environment with Docker Compose following the guide in [../development.md](../development.md). +Run the backend locally and connect it to PostgreSQL in Docker Compose. + +From the project root, start PostgreSQL and Mailcatcher: + +```console +$ docker compose up -d db mailcatcher +``` + +Then, from `./backend/`, install the dependencies, prepare the database, and start the development server: + +```console +$ uv sync +$ uv run bash scripts/prestart.sh +$ uv run fastapi dev +``` + +The API is available at `http://localhost:8000`, with automatic interactive docs at `http://localhost:8000/docs`. ## General Workflow @@ -35,66 +51,32 @@ There are already configurations in place to run the backend through the VS Code The setup is also already configured so you can run the tests through the VS Code Python tests tab. -## Docker Compose Override +## Full Stack with Docker Compose -During development, you can change Docker Compose settings that will only affect the local development environment in the file `compose.override.yml`. - -The changes to that file only affect the local development environment, not the production environment. So, you can add "temporary" changes that help the development workflow. - -For example, the directory with the backend code is synchronized in the Docker container, copying the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast. - -There is also a command override that runs `fastapi run --reload` instead of the default `fastapi run`. It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again: +To run the backend and built frontend in Docker Compose: ```console $ docker compose watch ``` -There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes. +The application is available at `http://localhost:8000`. -To get inside the container with a `bash` session you can start the stack with: +### Docker Compose Override -```console -$ docker compose watch -``` +The `compose.override.yml` file contains local settings for published ports, source synchronization, automatic image rebuilds, and backend reloads. Docker Compose applies it automatically when you run `docker compose` without an explicit file list. -and then in another terminal, `exec` inside the running container: +To open a shell in the backend container: ```console $ docker compose exec backend bash ``` -You should see an output like: - -```console -root@7f2607af31c3:/app# -``` - -that means that you are in a `bash` session inside your container, as a `root` user, under the `/app` directory, this directory has another directory called "app" inside, that's where your code lives inside the container: `/app/app`. - -There you can use the `fastapi run --reload` command to run the debug live reloading server. - -```console -$ fastapi run --reload app/main.py -``` - -...it will look like: - -```console -root@7f2607af31c3:/app# fastapi run --reload app/main.py -``` - -and then hit enter. That runs the live reloading server that auto reloads when it detects code changes. - -Nevertheless, if it doesn't detect a change but a syntax error, it will just stop with an error. But as the container is still alive and you are in a Bash session, you can quickly restart it after fixing the error, running the same command ("up arrow" and "Enter"). - -...this previous detail is what makes it useful to have the container alive doing nothing and then, in a Bash session, make it run the live reload server. - ## Backend tests -To test the backend run: +To test the backend from the `backend` directory, run: ```console -$ bash ./scripts/test.sh +$ uv run bash ./scripts/test.sh ``` The tests run with Pytest, modify and add tests to `./backend/tests/`. @@ -123,22 +105,14 @@ When the tests are run, a file `htmlcov/index.html` is generated, you can open i ## Migrations -As during local development your app directory is mounted as a volume inside the container, you can also run the migrations with `alembic` commands inside the container and the migration code will be in your app directory (instead of being only inside the container). So you can add it to your git repository. - -Make sure you create a "revision" of your models and that you "upgrade" your database with that revision every time you change them. As this is what will update the tables in your database. Otherwise, your application will have errors. - -* Start an interactive session in the backend container: - -```console -$ docker compose exec backend bash -``` +Make sure you create a revision of your models and upgrade the database with that revision every time you change them. From the `backend` directory, use `uv` to run Alembic against the PostgreSQL container: * Alembic is already configured to import your SQLModel models from `./backend/app/models.py`. -* After changing a model (for example, adding a column), inside the container, create a revision, e.g.: +* After changing a model (for example, adding a column), create a revision: ```console -$ alembic revision --autogenerate -m "Add column last_name to User model" +$ uv run alembic revision --autogenerate -m "Add column last_name to User model" ``` * Commit to the git repository the files generated in the alembic directory. @@ -146,7 +120,7 @@ $ alembic revision --autogenerate -m "Add column last_name to User model" * After creating the revision, run the migration in the database (this is what will actually change the database): ```console -$ alembic upgrade head +$ uv run alembic upgrade head ``` If you don't want to use migrations at all, uncomment the lines in the file at `./backend/app/core/db.py` that end in: diff --git a/compose.override.yml b/compose.override.yml index 2378d468..d09cf64f 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -71,7 +71,6 @@ services: SMTP_HOST: "mailcatcher" SMTP_PORT: "1025" SMTP_TLS: "false" - EMAILS_FROM_EMAIL: "noreply@example.com" mailcatcher: image: schickling/mailcatcher diff --git a/compose.yml b/compose.yml index bbc7aa65..3c5e73cd 100644 --- a/compose.yml +++ b/compose.yml @@ -70,9 +70,6 @@ services: SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL} - SMTP_TLS: ${SMTP_TLS} - SMTP_SSL: ${SMTP_SSL} - SMTP_PORT: ${SMTP_PORT} POSTGRES_SERVER: db POSTGRES_PORT: ${POSTGRES_PORT} POSTGRES_DB: ${POSTGRES_DB} diff --git a/development.md b/development.md index 576d3930..3a537b2d 100644 --- a/development.md +++ b/development.md @@ -1,14 +1,66 @@ # FastAPI Project - Development -## Docker Compose +## Local Development -* Start the local stack with Docker Compose: +For local development, run PostgreSQL and Mailcatcher with Docker Compose, and run the FastAPI and Vite development servers locally. + +Start the supporting services: + +```bash +docker compose up -d db mailcatcher +``` + +Then, from the `backend` directory, install the dependencies and prepare the database: + +```bash +uv sync +uv run bash scripts/prestart.sh +``` + +Start the FastAPI development server: + +```bash +uv run fastapi dev +``` + +In another terminal, from the project root, install the frontend dependencies and start the Vite development server: + +```bash +bun install +bun run dev +``` + +Now you can open these URLs: + +Frontend development server: + +Backend API: + +Automatic interactive API documentation with Swagger UI: + +Mailcatcher: + +The frontend development server uses the backend at `http://localhost:8000`, as configured in `frontend/.env`. + +### Frontend Served by FastAPI + +Build the frontend from the `frontend` directory: + +```bash +bun run build +``` + +The build is written to `backend/app/frontend` and served by FastAPI at . Rebuild the frontend after making frontend changes. + +## Full Stack with Docker Compose + +To run the backend and built frontend in Docker Compose: ```bash docker compose watch ``` -* Now you can open your browser and interact with these URLs: +Now you can open these URLs: Application, with the frontend and API served by FastAPI: @@ -18,58 +70,15 @@ Adminer, database web administration: Traefik UI, to see how the routes are being handled by the proxy: -**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it. +Mailcatcher: -To check the logs, run (in another terminal): +Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`. -```bash -docker compose logs -``` - -To check the logs of a specific service, add the name of the service, e.g.: - -```bash -docker compose logs backend -``` +**Note**: The first time you start the stack, it might take a minute to be ready while the backend waits for the database and configures everything. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service. ## Mailcatcher -Mailcatcher is a simple SMTP server that catches all emails sent by the backend during local development. Instead of sending real emails, they are captured and displayed in a web interface. - -This is useful for: - -* Testing email functionality during development -* Verifying email content and formatting -* Debugging email-related functionality without sending real emails - -The backend is automatically configured to use Mailcatcher when running with Docker Compose locally (SMTP on port 1025). All captured emails can be viewed at . - -## Local Development - -The Docker Compose files are configured so that each supporting service is available in a different port in `localhost`. - -FastAPI serves the built frontend and the API as one application at `http://localhost:8000`. The API routes live under `/api`. - -For frontend development with live reload, you can still run the local Vite development server separately. - -Start the local frontend development server with: - -```bash -bun run dev -``` - -Or you could stop the `backend` Docker Compose service: - -```bash -docker compose stop backend -``` - -And then you can run the local development server for the backend: - -```bash -cd backend -fastapi dev app/main.py -``` +Mailcatcher captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailcatcher` service. Captured emails are available at . ## Docker Compose in `localhost.tiangolo.com` @@ -107,7 +116,7 @@ And there's also a `compose.override.yml` with overrides for development, for ex The `compose.deploy.yml` file contains the deployment-specific settings, including HTTPS and automatic certificate handling. It is explicitly combined with `compose.yml` when deploying the application. -These Docker Compose files use the `.env` file containing configurations to be injected as environment variables in the containers. +The backend reads local settings from the `.env` file. Docker Compose also uses it for variable interpolation and passes the settings each container needs. They also use some additional configurations taken from environment variables set in the scripts before calling the `docker compose` command. @@ -119,11 +128,11 @@ docker compose watch ## The .env file -The `.env` file is the one that contains all your configurations, generated keys and passwords, etc. +The `.env` file contains the shared local defaults, generated keys, passwords, and other configuration. Its hostnames use `localhost` for processes running on your machine. Docker Compose overrides hostnames such as the database and SMTP server with their Compose service names. Depending on your workflow, you could want to exclude it from Git, for example if your project is public. In that case, you would have to make sure to set up a way for your CI tools to obtain it while building or deploying your project. -One way to do it could be to add each environment variable to your CI/CD system, and updating the `compose.yml` file to read that specific env var instead of reading the `.env` file. +One way to do it could be to add each environment variable to your CI/CD system. ## Pre-commits and code linting diff --git a/frontend/README.md b/frontend/README.md index 6def8e1c..6c34e820 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -15,7 +15,9 @@ bun run dev * Then open your browser at http://localhost:5173/. -Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the backend Docker image and start it, to test the production-like setup where FastAPI serves the built frontend at `http://localhost:8000`. +Run the backend locally with `uv run fastapi dev` and PostgreSQL in Docker Compose. See [../development.md](../development.md) for the complete setup. + +To serve the frontend with FastAPI, run `bun run build` from the `frontend` directory and open `http://localhost:8000`. To run the full stack with Docker Compose, use `docker compose watch`. Check the file `package.json` to see other available options. @@ -33,14 +35,6 @@ If you are developing an API-only app and want to remove the frontend, you can d Done, you have a frontend-less (api-only) app. 🤓 ---- - -If you want, you can also remove the `FRONTEND_HOST` environment variable from: - -* `.env` - -But it would be only to clean them up, leaving them won't really have any effect either way. - ## Generate Client ### Automatically @@ -56,9 +50,9 @@ bash ./scripts/generate-client.sh ### Manually -* Start the Docker Compose stack. +* Make sure the backend is running. -* Download the OpenAPI JSON file from `http://localhost/api/v1/openapi.json` and copy it to a new file `openapi.json` at the root of the `frontend` directory. +* Download the OpenAPI JSON file from `http://localhost:8000/api/v1/openapi.json` and copy it to a new file `openapi.json` at the root of the `frontend` directory. * To generate the frontend client, run: From 029e92925329c537dd48bd01e5b5a09cf27bb7c4 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:23:50 +0000 Subject: [PATCH 26/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 55714781..c27c868b 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify local development workflow. PR [#2414](https://github.com/fastapi/full-stack-fastapi-template/pull/2414) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify environment configuration. PR [#2413](https://github.com/fastapi/full-stack-fastapi-template/pull/2413) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify Traefik deployment, make it part of the stack. PR [#2412](https://github.com/fastapi/full-stack-fastapi-template/pull/2412) by [@tiangolo](https://github.com/tiangolo). * ♻️ Serve frontend from FastAPI. PR [#2393](https://github.com/fastapi/full-stack-fastapi-template/pull/2393) by [@tiangolo](https://github.com/tiangolo). From 4632845a39d790749b201dc01069eb1b7e0d1386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 17:39:59 +0200 Subject: [PATCH 27/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20environme?= =?UTF-8?q?nt=20defaults=20(#2415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .env | 5 ----- compose.override.yml | 6 +++--- compose.yml | 4 ---- deployment.md | 2 -- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.env b/.env index 1226cb23..00f188dc 100644 --- a/.env +++ b/.env @@ -5,10 +5,6 @@ DOMAIN=localhost # To test the local Traefik config # DOMAIN=localhost.tiangolo.com -# Public frontend URL, used by the backend to generate links in emails. -# Deployments derive this from DOMAIN in compose.deploy.yml. -FRONTEND_HOST=http://localhost:5173 - # Environment: local, staging, production ENVIRONMENT=local @@ -31,7 +27,6 @@ SMTP_PORT=1025 # Postgres POSTGRES_SERVER=localhost -POSTGRES_PORT=5432 POSTGRES_DB=app POSTGRES_USER=postgres POSTGRES_PASSWORD=changethis diff --git a/compose.override.yml b/compose.override.yml index d09cf64f..5fc0ec37 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -88,15 +88,15 @@ services: depends_on: - backend - mailcatcher - env_file: - - .env environment: + - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} + - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} - PLAYWRIGHT_BASE_URL=http://backend:8000 - VITE_API_URL=http://backend:8000 - MAILCATCHER_HOST=http://mailcatcher:1080 # For the reports when run locally - PLAYWRIGHT_HTML_HOST=0.0.0.0 - - CI=${CI} + - CI=${CI:-} volumes: - ./frontend/blob-report:/app/frontend/blob-report - ./frontend/test-results:/app/frontend/test-results diff --git a/compose.yml b/compose.yml index 3c5e73cd..e9b34254 100644 --- a/compose.yml +++ b/compose.yml @@ -27,8 +27,6 @@ services: timeout: 10s volumes: - app-db-data:/var/lib/postgresql - env_file: - - .env environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - POSTGRES_USER=${POSTGRES_USER?Variable not set} @@ -59,7 +57,6 @@ services: condition: service_completed_successfully # The YAML anchor lets the prestart service reuse this environment mapping. environment: &backend-environment - FRONTEND_HOST: ${FRONTEND_HOST?Variable not set} ENVIRONMENT: ${ENVIRONMENT} PROJECT_NAME: ${PROJECT_NAME?Variable not set} BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS} @@ -71,7 +68,6 @@ services: SMTP_PASSWORD: ${SMTP_PASSWORD} EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL} POSTGRES_SERVER: db - POSTGRES_PORT: ${POSTGRES_PORT} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER?Variable not set} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?Variable not set} diff --git a/deployment.md b/deployment.md index 1485c6ba..5f3dbb76 100644 --- a/deployment.md +++ b/deployment.md @@ -89,8 +89,6 @@ You can set several other environment variables: * `SMTP_USER`: The SMTP server user to send emails. * `SMTP_PASSWORD`: The SMTP server password to send emails. * `EMAILS_FROM_EMAIL`: The email account to send emails from. -* `POSTGRES_SERVER`: The hostname of the PostgreSQL server. You can leave the default of `db`, provided by the same Docker Compose. You normally wouldn't need to change this unless you are using a third-party provider. -* `POSTGRES_PORT`: The port of the PostgreSQL server. You can leave the default. You normally wouldn't need to change this unless you are using a third-party provider. * `POSTGRES_USER`: The Postgres user, you can leave the default. * `POSTGRES_DB`: The database name to use for this application. You can leave the default of `app`. * `SENTRY_DSN`: The DSN for Sentry, if you are using it. From 0625bcc8b0134fefb1ce2920bd0182a936834fbf Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:40:02 +0000 Subject: [PATCH 28/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index c27c868b..2f0c7907 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify environment defaults. PR [#2415](https://github.com/fastapi/full-stack-fastapi-template/pull/2415) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify local development workflow. PR [#2414](https://github.com/fastapi/full-stack-fastapi-template/pull/2414) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify environment configuration. PR [#2413](https://github.com/fastapi/full-stack-fastapi-template/pull/2413) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify Traefik deployment, make it part of the stack. PR [#2412](https://github.com/fastapi/full-stack-fastapi-template/pull/2412) by [@tiangolo](https://github.com/tiangolo). From 2c7e51505ed1622eb1a98b6d4cdd126a4aad6943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 17:56:33 +0200 Subject: [PATCH 29/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20Copier=20?= =?UTF-8?q?environment=20configuration=20(#2416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .copier/update_dotenv.py | 11 +++++++++++ README.md | 4 ---- copier.yml | 20 -------------------- deployment.md | 14 ++++++++++---- 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/.copier/update_dotenv.py b/.copier/update_dotenv.py index 65768856..ceb8eb22 100644 --- a/.copier/update_dotenv.py +++ b/.copier/update_dotenv.py @@ -9,9 +9,20 @@ answers_path = Path(__file__).parent / ".copier-answers.yml" answers = json.loads(answers_path.read_text()) env_path = root_path / ".env" env_content = env_path.read_text() +# Keep deployment provider settings from replacing local development defaults. +env_answer_keys = { + "emails_from_email", + "first_superuser", + "first_superuser_password", + "postgres_password", + "project_name", + "secret_key", +} lines = [] for line in env_content.splitlines(): for key, value in answers.items(): + if key not in env_answer_keys: + continue upper_key = key.upper() if line.startswith(f"{upper_key}="): if " " in value: diff --git a/README.md b/README.md index 9ea0f276..00fd3894 100644 --- a/README.md +++ b/README.md @@ -199,12 +199,8 @@ The input variables, with their default values (some auto generated) are: - `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above. - `first_superuser`: (default: `"admin@example.com"`) The email of the first superuser (in .env). - `first_superuser_password`: (default: `"changethis"`) The password of the first superuser (in .env). -- `smtp_host`: (default: "") The SMTP server host to send emails, you can set it later in .env. -- `smtp_user`: (default: "") The SMTP server user to send emails, you can set it later in .env. -- `smtp_password`: (default: "") The SMTP server password to send emails, you can set it later in .env. - `emails_from_email`: (default: `"info@example.com"`) The email account to send emails from, you can set it later in .env. - `postgres_password`: (default: `"changethis"`) The password for the PostgreSQL database, stored in .env, you can generate one with the method above. -- `sentry_dsn`: (default: "") The DSN for Sentry, if you are using it, you can set it later in .env. ## Backend Development diff --git a/copier.yml b/copier.yml index f28369e8..6d6c99a8 100644 --- a/copier.yml +++ b/copier.yml @@ -21,21 +21,6 @@ first_superuser_password: help: The password of the first superuser (in .env) default: changethis -smtp_host: - type: str - help: The SMTP server host to send emails, you can set it later in .env - default: "" - -smtp_user: - type: str - help: The SMTP server user to send emails, you can set it later in .env - default: "" - -smtp_password: - type: str - help: The SMTP server password to send emails, you can set it later in .env - default: "" - emails_from_email: type: str help: The email account to send emails from, you can set it later in .env @@ -49,11 +34,6 @@ postgres_password: python -c "import secrets; print(secrets.token_urlsafe(32))"' default: changethis -sentry_dsn: - type: str - help: The DSN for Sentry, if you are using it, you can set it later in .env - default: "" - _exclude: # Global - .vscode diff --git a/deployment.md b/deployment.md index 5f3dbb76..8cb4fcb1 100644 --- a/deployment.md +++ b/deployment.md @@ -200,16 +200,22 @@ To configure them, go to your repository's **Settings** > **Environments** and c For each GitHub Environment (`staging` and `production`), configure the required secrets as [environment secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-an-environment). Environment secrets are preferred over [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository) because they are scoped to the specific environment, reducing exposure and aligning with any protection rules you configure. -The current Github Actions workflows expect these secrets: +The deployment workflows require these secrets: * `DOMAIN` -* `EMAILS_FROM_EMAIL` * `FIRST_SUPERUSER` * `FIRST_SUPERUSER_PASSWORD` * `POSTGRES_PASSWORD` * `SECRET_KEY` -* `LATEST_CHANGES` -* `SMOKESHOW_AUTH_KEY` + +To enable emails, configure these additional secrets with the values from your email provider: + +* `SMTP_HOST` +* `SMTP_USER` +* `SMTP_PASSWORD` +* `EMAILS_FROM_EMAIL` + +To enable Sentry, configure the `SENTRY_DSN` secret. ## GitHub Action Deployment Workflows From bda03258181a11575ae3215167b7f05226de83d6 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:56:36 +0000 Subject: [PATCH 30/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 2f0c7907..396171a1 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify Copier environment configuration. PR [#2416](https://github.com/fastapi/full-stack-fastapi-template/pull/2416) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify environment defaults. PR [#2415](https://github.com/fastapi/full-stack-fastapi-template/pull/2415) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify local development workflow. PR [#2414](https://github.com/fastapi/full-stack-fastapi-template/pull/2414) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify environment configuration. PR [#2413](https://github.com/fastapi/full-stack-fastapi-template/pull/2413) by [@tiangolo](https://github.com/tiangolo). From dd15054da059792a6463b4187ae3900d074e290e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 18:17:29 +0200 Subject: [PATCH 31/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20optional?= =?UTF-8?q?=20environment=20configuration=20(#2417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .env | 7 ------- .github/workflows/deploy-production.yml | 1 - .github/workflows/deploy-staging.yml | 1 - backend/app/core/config.py | 2 +- compose.override.yml | 5 +++++ compose.yml | 9 ++++----- deployment.md | 6 ------ 7 files changed, 10 insertions(+), 21 deletions(-) diff --git a/.env b/.env index 00f188dc..4891a51c 100644 --- a/.env +++ b/.env @@ -10,17 +10,12 @@ ENVIRONMENT=local PROJECT_NAME="Full Stack FastAPI Project" -# Backend -# Additional allowed CORS origins, separated by commas -BACKEND_CORS_ORIGINS= SECRET_KEY=changethis FIRST_SUPERUSER=admin@example.com FIRST_SUPERUSER_PASSWORD=changethis # Emails SMTP_HOST=localhost -SMTP_USER= -SMTP_PASSWORD= EMAILS_FROM_EMAIL=info@example.com SMTP_TLS=False SMTP_PORT=1025 @@ -30,5 +25,3 @@ POSTGRES_SERVER=localhost POSTGRES_DB=app POSTGRES_USER=postgres POSTGRES_PASSWORD=changethis - -SENTRY_DSN= diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index cc4b34db..802bf8a3 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -17,7 +17,6 @@ jobs: - self-hosted - production env: - ENVIRONMENT: production DOMAIN: ${{ secrets.DOMAIN }} SECRET_KEY: ${{ secrets.SECRET_KEY }} FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 256bd0d2..561afddd 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -17,7 +17,6 @@ jobs: - self-hosted - staging env: - ENVIRONMENT: staging DOMAIN: ${{ secrets.DOMAIN }} SECRET_KEY: ${{ secrets.SECRET_KEY }} FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 87106962..f3a344c0 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -34,7 +34,7 @@ class Settings(BaseSettings): # 60 minutes * 24 hours * 8 days = 8 days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 FRONTEND_HOST: str = "http://localhost:5173" - ENVIRONMENT: Literal["local", "staging", "production"] = "local" + ENVIRONMENT: Literal["local"] | None = None BACKEND_CORS_ORIGINS: Annotated[ list[AnyUrl] | str, BeforeValidator(parse_cors) diff --git a/compose.override.yml b/compose.override.yml index 5fc0ec37..f056a2ec 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -68,10 +68,15 @@ services: volumes: - ./backend/htmlcov:/app/backend/htmlcov environment: + ENVIRONMENT: "local" SMTP_HOST: "mailcatcher" SMTP_PORT: "1025" SMTP_TLS: "false" + prestart: + environment: + ENVIRONMENT: "local" + mailcatcher: image: schickling/mailcatcher ports: diff --git a/compose.yml b/compose.yml index e9b34254..b59c2d57 100644 --- a/compose.yml +++ b/compose.yml @@ -57,21 +57,20 @@ services: condition: service_completed_successfully # The YAML anchor lets the prestart service reuse this environment mapping. environment: &backend-environment - ENVIRONMENT: ${ENVIRONMENT} PROJECT_NAME: ${PROJECT_NAME?Variable not set} - BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS} + BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS:-} SECRET_KEY: ${SECRET_KEY?Variable not set} FIRST_SUPERUSER: ${FIRST_SUPERUSER?Variable not set} FIRST_SUPERUSER_PASSWORD: ${FIRST_SUPERUSER_PASSWORD?Variable not set} SMTP_HOST: ${SMTP_HOST} - SMTP_USER: ${SMTP_USER} - SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL} POSTGRES_SERVER: db POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER?Variable not set} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?Variable not set} - SENTRY_DSN: ${SENTRY_DSN} + SENTRY_DSN: ${SENTRY_DSN:-} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"] diff --git a/deployment.md b/deployment.md index 8cb4fcb1..832d2d54 100644 --- a/deployment.md +++ b/deployment.md @@ -46,12 +46,6 @@ Copy the content and use that as password / secret key. And run that again to ge ### Required Environment Variables -Set the `ENVIRONMENT`, by default `local` (for development), but when deploying to a server you would put something like `staging` or `production`: - -```bash -export ENVIRONMENT=production -``` - Set the `DOMAIN`, by default `localhost` (for development), but when deploying you would use your own domain, for example: ```bash From 1a30f30e51a43a7cc4d80756d0ae83084030407f Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:17:31 +0000 Subject: [PATCH 32/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 396171a1..3370b831 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify optional environment configuration. PR [#2417](https://github.com/fastapi/full-stack-fastapi-template/pull/2417) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify Copier environment configuration. PR [#2416](https://github.com/fastapi/full-stack-fastapi-template/pull/2416) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify environment defaults. PR [#2415](https://github.com/fastapi/full-stack-fastapi-template/pull/2415) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify local development workflow. PR [#2414](https://github.com/fastapi/full-stack-fastapi-template/pull/2414) by [@tiangolo](https://github.com/tiangolo). From 060795204b2948cbfbd1eac88f01e4188697e041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 18:33:51 +0200 Subject: [PATCH 33/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Require=20non-empty?= =?UTF-8?q?=20Compose=20configuration=20(#2418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .env | 7 ------- compose.deploy.yml | 8 ++++---- compose.override.yml | 4 ++-- compose.yml | 24 ++++++++++++------------ deployment.md | 2 +- 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/.env b/.env index 4891a51c..c6f805a3 100644 --- a/.env +++ b/.env @@ -1,10 +1,3 @@ -# Domain -# This would be set to the production domain with an env var on deployment -# used by Traefik to transmit traffic and acquire TLS certificates -DOMAIN=localhost -# To test the local Traefik config -# DOMAIN=localhost.tiangolo.com - # Environment: local, staging, production ENVIRONMENT=local diff --git a/compose.deploy.yml b/compose.deploy.yml index a54c2d44..6ee6c9b0 100644 --- a/compose.deploy.yml +++ b/compose.deploy.yml @@ -38,7 +38,7 @@ services: restart: always labels: # Route HTTPS traffic for the Adminer subdomain - - traefik.http.routers.adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`) + - traefik.http.routers.adminer-https.rule=Host(`adminer.${DOMAIN:?Variable not set}`) - traefik.http.routers.adminer-https.entrypoints=https - traefik.http.routers.adminer-https.tls=true # Use the Let's Encrypt resolver @@ -46,15 +46,15 @@ services: prestart: environment: - FRONTEND_HOST: https://${DOMAIN?Variable not set} + FRONTEND_HOST: https://${DOMAIN:?Variable not set} backend: restart: always environment: - FRONTEND_HOST: https://${DOMAIN?Variable not set} + FRONTEND_HOST: https://${DOMAIN:?Variable not set} labels: # Route HTTPS traffic for this domain - - traefik.http.routers.backend-https.rule=Host(`${DOMAIN?Variable not set}`) + - traefik.http.routers.backend-https.rule=Host(`${DOMAIN:?Variable not set}`) - traefik.http.routers.backend-https.entrypoints=https - traefik.http.routers.backend-https.tls=true # Use the Let's Encrypt resolver diff --git a/compose.override.yml b/compose.override.yml index f056a2ec..2206fe26 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -94,8 +94,8 @@ services: - backend - mailcatcher environment: - - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} - - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} + - FIRST_SUPERUSER=${FIRST_SUPERUSER:?Variable not set} + - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD:?Variable not set} - PLAYWRIGHT_BASE_URL=http://backend:8000 - VITE_API_URL=http://backend:8000 - MAILCATCHER_HOST=http://mailcatcher:1080 diff --git a/compose.yml b/compose.yml index b59c2d57..0d164713 100644 --- a/compose.yml +++ b/compose.yml @@ -28,9 +28,9 @@ services: volumes: - app-db-data:/var/lib/postgresql environment: - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - - POSTGRES_USER=${POSTGRES_USER?Variable not set} - - POSTGRES_DB=${POSTGRES_DB?Variable not set} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Variable not set} + - POSTGRES_USER=${POSTGRES_USER:?Variable not set} + - POSTGRES_DB=${POSTGRES_DB:?Variable not set} adminer: image: adminer @@ -42,7 +42,7 @@ services: # Enable Traefik for this service - traefik.enable=true # Route HTTP traffic for the Adminer subdomain - - traefik.http.routers.adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`) + - traefik.http.routers.adminer-http.rule=Host(`adminer.${DOMAIN:-localhost}`) - traefik.http.routers.adminer-http.entrypoints=http # Define the port inside of the Docker service to use - traefik.http.services.adminer.loadbalancer.server.port=8080 @@ -57,19 +57,19 @@ services: condition: service_completed_successfully # The YAML anchor lets the prestart service reuse this environment mapping. environment: &backend-environment - PROJECT_NAME: ${PROJECT_NAME?Variable not set} + PROJECT_NAME: ${PROJECT_NAME:?Variable not set} BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS:-} - SECRET_KEY: ${SECRET_KEY?Variable not set} - FIRST_SUPERUSER: ${FIRST_SUPERUSER?Variable not set} - FIRST_SUPERUSER_PASSWORD: ${FIRST_SUPERUSER_PASSWORD?Variable not set} + SECRET_KEY: ${SECRET_KEY:?Variable not set} + FIRST_SUPERUSER: ${FIRST_SUPERUSER:?Variable not set} + FIRST_SUPERUSER_PASSWORD: ${FIRST_SUPERUSER_PASSWORD:?Variable not set} SMTP_HOST: ${SMTP_HOST} SMTP_USER: ${SMTP_USER:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL} POSTGRES_SERVER: db - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER?Variable not set} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?Variable not set} + POSTGRES_DB: ${POSTGRES_DB:?Variable not set} + POSTGRES_USER: ${POSTGRES_USER:?Variable not set} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Variable not set} SENTRY_DSN: ${SENTRY_DSN:-} healthcheck: @@ -89,7 +89,7 @@ services: - traefik.http.services.backend.loadbalancer.server.port=8000 # Route HTTP traffic for this domain - - traefik.http.routers.backend-http.rule=Host(`${DOMAIN?Variable not set}`) + - traefik.http.routers.backend-http.rule=Host(`${DOMAIN:-localhost}`) - traefik.http.routers.backend-http.entrypoints=http prestart: diff --git a/deployment.md b/deployment.md index 832d2d54..48df8b51 100644 --- a/deployment.md +++ b/deployment.md @@ -46,7 +46,7 @@ Copy the content and use that as password / secret key. And run that again to ge ### Required Environment Variables -Set the `DOMAIN`, by default `localhost` (for development), but when deploying you would use your own domain, for example: +Set the `DOMAIN` to your own domain, for example: ```bash export DOMAIN=fastapi-project.example.com From a5aff58850ab7883c64d920063e6ab33ce71e08e Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:33:53 +0000 Subject: [PATCH 34/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 3370b831..1c5fcbbc 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Require non-empty Compose configuration. PR [#2418](https://github.com/fastapi/full-stack-fastapi-template/pull/2418) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify optional environment configuration. PR [#2417](https://github.com/fastapi/full-stack-fastapi-template/pull/2417) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify Copier environment configuration. PR [#2416](https://github.com/fastapi/full-stack-fastapi-template/pull/2416) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify environment defaults. PR [#2415](https://github.com/fastapi/full-stack-fastapi-template/pull/2415) by [@tiangolo](https://github.com/tiangolo). From 2c28c6f7dce687d4240da0cbc481a10b0e2ce9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 18:46:19 +0200 Subject: [PATCH 35/91] =?UTF-8?q?=F0=9F=93=9D=20Clarify=20environment=20co?= =?UTF-8?q?nfiguration=20(#2419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .env | 2 +- deployment.md | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.env b/.env index c6f805a3..f6c896a4 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -# Environment: local, staging, production +# Enable local-only development behavior ENVIRONMENT=local PROJECT_NAME="Full Stack FastAPI Project" diff --git a/deployment.md b/deployment.md index 48df8b51..dcb9d66e 100644 --- a/deployment.md +++ b/deployment.md @@ -54,24 +54,22 @@ export DOMAIN=fastapi-project.example.com The deployment Docker Compose configuration also uses `DOMAIN` to set the public frontend URL used in links generated by the backend. -Set the `POSTGRES_PASSWORD` to something different than `changethis`: +Set the `POSTGRES_PASSWORD` to a secure value: ```bash -export POSTGRES_PASSWORD="changethis" +export POSTGRES_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" ``` -Set the `SECRET_KEY`, used to sign tokens: +Set the `SECRET_KEY`, used to sign tokens, to a secure value: ```bash -export SECRET_KEY="changethis" +export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" ``` -Note: you can use the Python command above to generate a secure secret key. - -Set the `FIRST_SUPERUSER_PASSWORD` to something different than `changethis`: +Set the `FIRST_SUPERUSER_PASSWORD` to a secure value: ```bash -export FIRST_SUPERUSER_PASSWORD="changethis" +export FIRST_SUPERUSER_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" ``` You can set several other environment variables: From 6c2122e37df0fb01890fa6559d5805d5f52c81f8 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:46:22 +0000 Subject: [PATCH 36/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 1c5fcbbc..7ae2e79a 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Docs +* 📝 Clarify environment configuration. PR [#2419](https://github.com/fastapi/full-stack-fastapi-template/pull/2419) by [@tiangolo](https://github.com/tiangolo). * 📝 Improve README screenshot alt text. PR [#2359](https://github.com/fastapi/full-stack-fastapi-template/pull/2359) by [@Bmowville](https://github.com/Bmowville). * ✏️ Fix typo in the comment for DOMAIN in the .env file. PR [#2305](https://github.com/fastapi/full-stack-fastapi-template/pull/2305) by [@serhiiur](https://github.com/serhiiur). * 📝 Update security policy. PR [#2297](https://github.com/fastapi/full-stack-fastapi-template/pull/2297) by [@tiangolo](https://github.com/tiangolo). From 375af70ee2729159820bd720ce1f9e5f4d9fbc9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 19:43:11 +0200 Subject: [PATCH 37/91] =?UTF-8?q?=E2=9C=85=20Fix=20sign-up=20test=20race?= =?UTF-8?q?=20(#2422)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- frontend/tests/sign-up.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/tests/sign-up.spec.ts b/frontend/tests/sign-up.spec.ts index 2a09e7c9..663020bf 100644 --- a/frontend/tests/sign-up.spec.ts +++ b/frontend/tests/sign-up.spec.ts @@ -79,15 +79,16 @@ test("Sign up with existing email", async ({ page }) => { await fillForm(page, fullName, email, password, password) await page.getByRole("button", { name: "Sign Up" }).click() + await expect(page).toHaveURL(/\/login$/) await page.goto("/signup") await fillForm(page, fullName, email, password, password) await page.getByRole("button", { name: "Sign Up" }).click() - await page - .getByText("The user with this email already exists in the system") - .click() + await expect( + page.getByText("The user with this email already exists in the system"), + ).toBeVisible() }) test("Sign up with weak password", async ({ page }) => { From de53c8fd35a196f7d4b2f7dd5dac87adb5cb1dce Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:43:14 +0000 Subject: [PATCH 38/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 7ae2e79a..30d56f14 100644 --- a/release-notes.md +++ b/release-notes.md @@ -7,6 +7,10 @@ * 🔧 Use FastAPI entrypoint. PR [#2360](https://github.com/fastapi/full-stack-fastapi-template/pull/2360) by [@tiangolo](https://github.com/tiangolo). * ♻️ Add library-skills for FastAPI and SQLModel. PR [#2354](https://github.com/fastapi/full-stack-fastapi-template/pull/2354) by [@tiangolo](https://github.com/tiangolo). +### Fixes + +* ✅ Fix sign-up test race. PR [#2422](https://github.com/fastapi/full-stack-fastapi-template/pull/2422) by [@tiangolo](https://github.com/tiangolo). + ### Refactors * ♻️ Require non-empty Compose configuration. PR [#2418](https://github.com/fastapi/full-stack-fastapi-template/pull/2418) by [@tiangolo](https://github.com/tiangolo). From b0ccf1e80ecdfd19a979baabe9b3036fd22f5915 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:59:35 +0200 Subject: [PATCH 39/91] =?UTF-8?q?=E2=9C=A8=20Add=20react-email=20for=20ema?= =?UTF-8?q?il=20templates=20(#2421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- .vscode/extensions.json | 1 - backend/README.md | 18 +- .../email-templates/build/new_account.html | 25 - .../email-templates/build/reset_password.html | 25 - .../app/email-templates/build/test_email.html | 25 - backend/app/email-templates/new_account.html | 1 + .../app/email-templates/reset_password.html | 1 + .../app/email-templates/src/new_account.mjml | 15 - .../email-templates/src/reset_password.mjml | 17 - .../app/email-templates/src/test_email.mjml | 11 - backend/app/email-templates/test_email.html | 1 + backend/app/utils.py | 2 +- bun.lock | 506 ++++++++++++++++-- package.json | 7 +- packages/react-email/biome.json | 3 + packages/react-email/emails/new_account.tsx | 51 ++ .../react-email/emails/reset_password.tsx | 47 ++ packages/react-email/emails/test_email.tsx | 35 ++ packages/react-email/package.json | 23 + packages/react-email/tsconfig.json | 14 + packages/react-email/ui/Button.tsx | 12 + packages/react-email/ui/Callout.tsx | 22 + packages/react-email/ui/Heading.tsx | 12 + packages/react-email/ui/Layout.tsx | 85 +++ packages/react-email/ui/Link.tsx | 7 + 26 files changed, 802 insertions(+), 166 deletions(-) delete mode 100644 backend/app/email-templates/build/new_account.html delete mode 100644 backend/app/email-templates/build/reset_password.html delete mode 100644 backend/app/email-templates/build/test_email.html create mode 100644 backend/app/email-templates/new_account.html create mode 100644 backend/app/email-templates/reset_password.html delete mode 100644 backend/app/email-templates/src/new_account.mjml delete mode 100644 backend/app/email-templates/src/reset_password.mjml delete mode 100644 backend/app/email-templates/src/test_email.mjml create mode 100644 backend/app/email-templates/test_email.html create mode 100644 packages/react-email/biome.json create mode 100644 packages/react-email/emails/new_account.tsx create mode 100644 packages/react-email/emails/reset_password.tsx create mode 100644 packages/react-email/emails/test_email.tsx create mode 100644 packages/react-email/package.json create mode 100644 packages/react-email/tsconfig.json create mode 100644 packages/react-email/ui/Button.tsx create mode 100644 packages/react-email/ui/Callout.tsx create mode 100644 packages/react-email/ui/Heading.tsx create mode 100644 packages/react-email/ui/Layout.tsx create mode 100644 packages/react-email/ui/Link.tsx diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74c121c2..8f142621 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: exclude: | (?x)^( frontend/src/client/.*| - backend/app/email-templates/build/.* + backend/app/email-templates/.* )$ - id: trailing-whitespace exclude: ^frontend/src/client/.* diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ade6e4d9..bc089f95 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,7 +7,6 @@ "charliermarsh.ruff", "docker.docker", "github.vscode-github-actions", - "mjmlio.vscode-mjml", "ms-playwright.playwright", "ms-python.python", "tombi-toml.tombi" diff --git a/backend/README.md b/backend/README.md index d59c7a50..308aa6a8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -139,8 +139,20 @@ If you don't want to start with the default models and want to remove them / mod ## Email Templates -The email templates are in `./backend/app/email-templates/`. Here, there are two directories: `build` and `src`. The `src` directory contains the source files that are used to build the final email templates. The `build` directory contains the final email templates that are used by the application. +The email templates are written with [React Email](https://react.email) in `./packages/react-email/`. The `emails` directory holds one component per email and the `ui` directory holds the shared components (layout, heading, button, link, callout). -Before continuing, ensure you have the [MJML extension](https://github.com/mjmlio/vscode-mjml) installed in your VS Code. +The rendered HTML in `./backend/app/email-templates/` is generated from those components, it is what the application sends, and it shouldn't be edited by hand. -Once you have the MJML extension installed, you can create a new email template in the `src` directory. After creating the new email template and with the `.mjml` file open in your editor, open the command palette with `Ctrl+Shift+P` and search for `MJML: Export to HTML`. This will convert the `.mjml` file to a `.html` file and now you can save it in the build directory. +To preview the emails while editing them, start the dev server from the root of the project: + +```console +$ bun run email:dev +``` + +Values coming from the backend are declared as Jinja placeholders in the component props, for example `username = "{{ username }}"`. The context for each email is built in `generate_*_email()` in `./backend/app/utils.py`, so a new placeholder needs to be added there too. + +Once you are done, regenerate the templates used by the application: + +```console +$ bun run email:export +``` diff --git a/backend/app/email-templates/build/new_account.html b/backend/app/email-templates/build/new_account.html deleted file mode 100644 index 34450503..00000000 --- a/backend/app/email-templates/build/new_account.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }} - New Account
Welcome to your new account!
Here are your account details:
Username: {{ username }}
Password: {{ password }}
Go to Dashboard

\ No newline at end of file diff --git a/backend/app/email-templates/build/reset_password.html b/backend/app/email-templates/build/reset_password.html deleted file mode 100644 index 4148a5b7..00000000 --- a/backend/app/email-templates/build/reset_password.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }} - Password Recovery
Hello {{ username }}
We've received a request to reset your password. You can do it by clicking the button below:
Reset password
Or copy and paste the following link into your browser:
This password will expire in {{ valid_hours }} hours.

If you didn't request a password recovery you can disregard this email.
\ No newline at end of file diff --git a/backend/app/email-templates/build/test_email.html b/backend/app/email-templates/build/test_email.html deleted file mode 100644 index 04d0d850..00000000 --- a/backend/app/email-templates/build/test_email.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }}
Test email for: {{ email }}

\ No newline at end of file diff --git a/backend/app/email-templates/new_account.html b/backend/app/email-templates/new_account.html new file mode 100644 index 00000000..1368a128 --- /dev/null +++ b/backend/app/email-templates/new_account.html @@ -0,0 +1 @@ +{{ project_name }} - New account
Your {{ project_name }} account is ready
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Welcome to {{ project_name }}!

Hi,

Your account has been successfully created and is ready to use. These are your credentials:

Username{{ username }}

Password{{ password }}

Get started by signing in to your dashboard:

Go to Dashboard

Or copy and paste this link into your browser:
{{ link }}

For security reasons, change your password after your first sign in.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file diff --git a/backend/app/email-templates/reset_password.html b/backend/app/email-templates/reset_password.html new file mode 100644 index 00000000..5b4d76bc --- /dev/null +++ b/backend/app/email-templates/reset_password.html @@ -0,0 +1 @@ +{{ project_name }} - Password recovery
Reset your {{ project_name }} password
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Reset your password

Hi {{ username }},

We've received a request to reset the password for your {{ project_name }} account. Choose a new one by clicking the button below:

Reset password

Or copy and paste this link into your browser:
{{ link }}

This link will expire in {{ valid_hours }} hours.

If you didn't request a password recovery, you can safely ignore this email.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file diff --git a/backend/app/email-templates/src/new_account.mjml b/backend/app/email-templates/src/new_account.mjml deleted file mode 100644 index f41a3e3c..00000000 --- a/backend/app/email-templates/src/new_account.mjml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - {{ project_name }} - New Account - Welcome to your new account! - Here are your account details: - Username: {{ username }} - Password: {{ password }} - Go to Dashboard - - - - - diff --git a/backend/app/email-templates/src/reset_password.mjml b/backend/app/email-templates/src/reset_password.mjml deleted file mode 100644 index 743f5d77..00000000 --- a/backend/app/email-templates/src/reset_password.mjml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - {{ project_name }} - Password Recovery - Hello {{ username }} - We've received a request to reset your password. You can do it by clicking the button below: - Reset password - Or copy and paste the following link into your browser: - {{ link }} - This password will expire in {{ valid_hours }} hours. - - If you didn't request a password recovery you can disregard this email. - - - - diff --git a/backend/app/email-templates/src/test_email.mjml b/backend/app/email-templates/src/test_email.mjml deleted file mode 100644 index 45d58d6b..00000000 --- a/backend/app/email-templates/src/test_email.mjml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - {{ project_name }} - Test email for: {{ email }} - - - - - diff --git a/backend/app/email-templates/test_email.html b/backend/app/email-templates/test_email.html new file mode 100644 index 00000000..d0fe62fe --- /dev/null +++ b/backend/app/email-templates/test_email.html @@ -0,0 +1 @@ +{{ project_name }} - Test email
{{ project_name }} email delivery is working
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Test email

Hi,

This is a test email from {{ project_name }}. If you're reading it, email delivery is configured correctly.

Sent to{{ email }}

If you weren't expecting this email, you can safely ignore it.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file diff --git a/backend/app/utils.py b/backend/app/utils.py index 687b7309..f6a2f40a 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -24,7 +24,7 @@ class EmailData: def render_email_template(*, template_name: str, context: dict[str, Any]) -> str: template_str = ( - Path(__file__).parent / "email-templates" / "build" / template_name + Path(__file__).parent / "email-templates" / template_name ).read_text() html_content = Template(template_str).render(context) return html_content diff --git a/bun.lock b/bun.lock index 76c17b6b..a5cf0d5d 100644 --- a/bun.lock +++ b/bun.lock @@ -60,15 +60,31 @@ "vite": "^8.0.16", }, }, + "packages/react-email": { + "name": "emails", + "version": "0.1.0", + "dependencies": { + "@react-email/components": "1.0.12", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-email": "6.9.1", + }, + "devDependencies": { + "@biomejs/biome": "^2.4.16", + "@react-email/ui": "6.9.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + }, + }, }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], @@ -86,11 +102,11 @@ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], @@ -118,57 +134,57 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], @@ -192,6 +208,56 @@ "@hookform/resolvers": ["@hookform/resolvers@5.4.0", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -208,6 +274,24 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], "@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="], @@ -292,6 +376,50 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + "@react-email/body": ["@react-email/body@0.3.0", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug=="], + + "@react-email/button": ["@react-email/button@0.2.1", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A=="], + + "@react-email/code-block": ["@react-email/code-block@0.2.1", "", { "dependencies": { "prismjs": "^1.30.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw=="], + + "@react-email/code-inline": ["@react-email/code-inline@0.0.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA=="], + + "@react-email/column": ["@react-email/column@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg=="], + + "@react-email/components": ["@react-email/components@1.0.12", "", { "dependencies": { "@react-email/body": "0.3.0", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/column": "0.0.14", "@react-email/container": "0.0.16", "@react-email/font": "0.0.10", "@react-email/head": "0.0.13", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/html": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/markdown": "0.0.18", "@react-email/preview": "0.0.14", "@react-email/render": "2.0.6", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", "@react-email/tailwind": "2.0.7", "@react-email/text": "0.1.6" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ=="], + + "@react-email/container": ["@react-email/container@0.0.16", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ=="], + + "@react-email/font": ["@react-email/font@0.0.10", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA=="], + + "@react-email/head": ["@react-email/head@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog=="], + + "@react-email/heading": ["@react-email/heading@0.0.16", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw=="], + + "@react-email/hr": ["@react-email/hr@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA=="], + + "@react-email/html": ["@react-email/html@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw=="], + + "@react-email/img": ["@react-email/img@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ=="], + + "@react-email/link": ["@react-email/link@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw=="], + + "@react-email/markdown": ["@react-email/markdown@0.0.18", "", { "dependencies": { "marked": "^15.0.12" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg=="], + + "@react-email/preview": ["@react-email/preview@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw=="], + + "@react-email/render": ["@react-email/render@2.0.6", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog=="], + + "@react-email/row": ["@react-email/row@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw=="], + + "@react-email/section": ["@react-email/section@0.0.17", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w=="], + + "@react-email/tailwind": ["@react-email/tailwind@2.0.7", "", { "dependencies": { "tailwindcss": "^4.1.18" }, "peerDependencies": { "@react-email/body": ">=0", "@react-email/button": ">=0", "@react-email/code-block": ">=0", "@react-email/code-inline": ">=0", "@react-email/container": ">=0", "@react-email/heading": ">=0", "@react-email/hr": ">=0", "@react-email/img": ">=0", "@react-email/link": ">=0", "@react-email/preview": ">=0", "@react-email/text": ">=0", "react": "^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@react-email/body", "@react-email/button", "@react-email/code-block", "@react-email/code-inline", "@react-email/container", "@react-email/heading", "@react-email/hr", "@react-email/img", "@react-email/link", "@react-email/preview"] }, "sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA=="], + + "@react-email/text": ["@react-email/text@0.1.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw=="], + + "@react-email/ui": ["@react-email/ui@6.9.1", "", { "dependencies": { "esbuild": "0.28.1", "next": "16.2.6" } }, "sha512-zw7KvwMu3RU/fDC9TAkegg9/qNV9foJtd3QRoetdWVFe8aQC/NyXmWTrG9IrkzVLzn5otkkxu7e7B1QelcwdiQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], @@ -324,6 +452,10 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], + + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], "@swc/core": ["@swc/core@1.15.11", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.11", "@swc/core-darwin-x64": "1.15.11", "@swc/core-linux-arm-gnueabihf": "1.15.11", "@swc/core-linux-arm64-gnu": "1.15.11", "@swc/core-linux-arm64-musl": "1.15.11", "@swc/core-linux-x64-gnu": "1.15.11", "@swc/core-linux-x64-musl": "1.15.11", "@swc/core-win32-arm64-msvc": "1.15.11", "@swc/core-win32-ia32-msvc": "1.15.11", "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w=="], @@ -350,6 +482,8 @@ "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="], "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], @@ -420,6 +554,8 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], @@ -428,10 +564,18 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.3.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], @@ -442,11 +586,19 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], + "axios": ["axios@1.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.16", "", { "bin": "dist/cli.js" }, "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA=="], + + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], @@ -458,30 +610,48 @@ "caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="], - "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-support": ["color-support@1.1.3", "", { "bin": "bin.js" }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "debounce": ["debounce@2.2.0", "", {}, "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw=="], + + "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -500,14 +670,34 @@ "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dot-prop": ["dot-prop@10.2.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "emails": ["emails@workspace:packages/react-email"], + + "engine.io": ["engine.io@6.6.9", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0" } }, "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + "enhanced-resolve": ["enhanced-resolve@5.24.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -516,12 +706,16 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": "bin/esbuild" }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "exsolve": ["exsolve@1.1.0", "", {}, "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], @@ -546,6 +740,8 @@ "giget": ["giget@3.3.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw=="], + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -558,6 +754,12 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="], + + "html5parser": ["html5parser@3.0.0", "", {}, "sha512-iNpSopa+4YHX50UOk825tBy7MghmXHo/ZpLskBYN0kAr1xhH8GlIMk5bLRXcZlfP3AnLUcSuFMu8C4MdOUxA8A=="], + + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "is-docker": ["is-docker@3.0.0", "", { "bin": "cli.js" }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -566,6 +768,8 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": "cli.js" }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "isbot": ["isbot@5.1.33", "", {}, "sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg=="], @@ -580,8 +784,16 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -606,40 +818,72 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picospinner": ["picospinner@3.1.2", "", {}, "sha512-0Z++uU8mvB0EvCPAEUq9BGiPcSravzJ+RPdXfjYlzXffqsogisiKRQqI6ctrSSJ6n985vxrgKtQ5n4sRINcJZg=="], + "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="], @@ -652,6 +896,10 @@ "prettier": ["prettier@3.8.0", "", { "bin": "bin/prettier.cjs" }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], @@ -660,6 +908,8 @@ "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "react-email": ["react-email@6.9.1", "", { "dependencies": { "@babel/parser": "7.29.2", "@babel/traverse": "7.29.0", "@react-email/render": ">=2.1.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "conf": "^15.0.2", "css-tree": "3.2.1", "debounce": "^2.0.0", "esbuild": "^0.28.0", "glob": "^13.0.6", "jiti": "2.6.1", "log-symbols": "^7.0.0", "marked": "^15.0.12", "mime-types": "^3.0.0", "normalize-path": "^3.0.0", "nypm": "0.6.6", "picospinner": "^3.0.0", "prismjs": "^1.30.0", "prompts": "2.4.2", "socket.io": "^4.8.1", "tailwindcss": "^4.1.18", "tsconfig-paths": "4.2.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" }, "bin": { "email": "./dist/cli/index.mjs" } }, "sha512-uUDRgFukMUXRlrsCNGlA0PZuUlQ44faI9hT/D7uMjozdumLBdHjfQttQswRYLjyLW8fFtg2HBrxAESbFU4ZKKA=="], + "react-error-boundary": ["react-error-boundary@6.1.2", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng=="], "react-hook-form": ["react-hook-form@7.78.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA=="], @@ -672,7 +922,9 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -682,36 +934,66 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], + + "socket.io-adapter": ["socket.io-adapter@2.5.8", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.21.0" } }, "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw=="], + + "socket.io-parser": ["socket.io-parser@4.2.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], + + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], @@ -724,22 +1006,62 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/core/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/core/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/core/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/core/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/generator/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/generator/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/parser/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@babel/template/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/traverse/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@hey-api/openapi-ts/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], @@ -754,24 +1076,128 @@ "@tanstack/router-generator/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "@tanstack/router-plugin/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@tanstack/router-utils/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "babel-dead-code-elimination/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.16", "", { "bin": "dist/cli.js" }, "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw=="], "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "es-set-tostringtag/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "get-intrinsic/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "react-email/@react-email/render": ["@react-email/render@2.1.0", "", { "dependencies": { "entities": "^4.5.0", "html-to-text": "^9.0.5", "html5parser": "^3.0.0", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F+zE3O6d6sW6Aj2UjvZAA17R7tJKM7kcq2mgV6k4HCT8jeLLFaVP2txMtH1lgqYFRMZ0Gxsd37q2PRyiXLXXxA=="], + + "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": "bin/esbuild" }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "tsx/get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "unplugin/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "@tanstack/router-utils/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "babel-dead-code-elimination/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "babel-dead-code-elimination/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], } } diff --git a/package.json b/package.json index ab558ed4..862ae080 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,15 @@ "name": "fastapi-full-stack-template", "private": true, "workspaces": [ - "frontend" + "frontend", + "packages/*" ], "scripts": { "dev": "bun run --filter frontend dev", "lint": "bun run --filter frontend lint", "test": "bun run --filter frontend test", - "test:ui": "bun run --filter frontend test:ui" + "test:ui": "bun run --filter frontend test:ui", + "email:dev": "bun run --filter emails dev", + "email:export": "bun run --filter emails export" } } diff --git a/packages/react-email/biome.json b/packages/react-email/biome.json new file mode 100644 index 00000000..c7811a32 --- /dev/null +++ b/packages/react-email/biome.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../frontend/biome.json"] +} diff --git a/packages/react-email/emails/new_account.tsx b/packages/react-email/emails/new_account.tsx new file mode 100644 index 00000000..aae6f240 --- /dev/null +++ b/packages/react-email/emails/new_account.tsx @@ -0,0 +1,51 @@ +import { Text } from "@react-email/components" +import { LinkButton } from "../ui/Button" +import { Callout, Detail } from "../ui/Callout" +import { Heading } from "../ui/Heading" +import { Layout } from "../ui/Layout" +import { Link } from "../ui/Link" + +type NewAccountProps = { + project_name: string + username: string + password: string + link: string +} + +export default function NewAccount({ + project_name = "{{ project_name }}", + username = "{{ username }}", + password = "{{ password }}", + link = "{{ link }}", +}: NewAccountProps) { + return ( + + Welcome to {project_name}! + Hi, + + Your account has been successfully created and is ready to use. These + are your credentials: + + + + + + + Get started by signing in to your dashboard: + + Go to Dashboard + + Or copy and paste this link into your browser: +
+ {link} +
+ + For security reasons, change your password after your first sign in. + +
+ ) +} diff --git a/packages/react-email/emails/reset_password.tsx b/packages/react-email/emails/reset_password.tsx new file mode 100644 index 00000000..2dba207c --- /dev/null +++ b/packages/react-email/emails/reset_password.tsx @@ -0,0 +1,47 @@ +import { Text } from "@react-email/components" +import { LinkButton } from "../ui/Button" +import { Heading } from "../ui/Heading" +import { Layout } from "../ui/Layout" +import { Link } from "../ui/Link" + +type ResetPasswordProps = { + project_name: string + username: string + link: string + valid_hours: string +} + +export default function ResetPassword({ + project_name = "{{ project_name }}", + username = "{{ username }}", + link = "{{ link }}", + valid_hours = "{{ valid_hours }}", +}: ResetPasswordProps) { + return ( + + Reset your password + Hi {username}, + + We've received a request to reset the password for your {project_name}{" "} + account. Choose a new one by clicking the button below: + + Reset password + + Or copy and paste this link into your browser: +
+ {link} +
+ + This link will expire in {valid_hours} hours. + + + If you didn't request a password recovery, you can safely ignore this + email. + +
+ ) +} diff --git a/packages/react-email/emails/test_email.tsx b/packages/react-email/emails/test_email.tsx new file mode 100644 index 00000000..b3914287 --- /dev/null +++ b/packages/react-email/emails/test_email.tsx @@ -0,0 +1,35 @@ +import { Text } from "@react-email/components" +import { Callout, Detail } from "../ui/Callout" +import { Heading } from "../ui/Heading" +import { Layout } from "../ui/Layout" + +type TestEmailProps = { + project_name: string + email: string +} + +export default function TestEmail({ + project_name = "{{ project_name }}", + email = "{{ email }}", +}: TestEmailProps) { + return ( + + Test email + Hi, + + This is a test email from {project_name}. If you're reading it, email + delivery is configured correctly. + + + + + + If you weren't expecting this email, you can safely ignore it. + + + ) +} diff --git a/packages/react-email/package.json b/packages/react-email/package.json new file mode 100644 index 00000000..8b12cfbf --- /dev/null +++ b/packages/react-email/package.json @@ -0,0 +1,23 @@ +{ + "name": "emails", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "email build", + "dev": "email dev --port 3333", + "lint": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./", + "export": "email export --outDir ../../backend/app/email-templates" + }, + "dependencies": { + "@react-email/components": "1.0.12", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-email": "6.9.1" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.16", + "@react-email/ui": "6.9.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3" + } +} diff --git a/packages/react-email/tsconfig.json b/packages/react-email/tsconfig.json new file mode 100644 index 00000000..d4406870 --- /dev/null +++ b/packages/react-email/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "composite": false, + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "strictNullChecks": true + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", ".react-email"] +} diff --git a/packages/react-email/ui/Button.tsx b/packages/react-email/ui/Button.tsx new file mode 100644 index 00000000..fff4a68d --- /dev/null +++ b/packages/react-email/ui/Button.tsx @@ -0,0 +1,12 @@ +import { Button } from "@react-email/components" + +type ButtonProps = React.PropsWithChildren & { href: string } + +export const LinkButton = ({ children, href }: ButtonProps) => ( + +) diff --git a/packages/react-email/ui/Callout.tsx b/packages/react-email/ui/Callout.tsx new file mode 100644 index 00000000..60a96d7d --- /dev/null +++ b/packages/react-email/ui/Callout.tsx @@ -0,0 +1,22 @@ +import { Section, Text } from "@react-email/components" + +type CalloutProps = React.PropsWithChildren + +export const Callout = ({ children }: CalloutProps) => ( +
+ {children} +
+) + +type DetailProps = { label: string; value: string } + +export const Detail = ({ label, value }: DetailProps) => ( + + + {label} + + + {value} + + +) diff --git a/packages/react-email/ui/Heading.tsx b/packages/react-email/ui/Heading.tsx new file mode 100644 index 00000000..6f8565c4 --- /dev/null +++ b/packages/react-email/ui/Heading.tsx @@ -0,0 +1,12 @@ +import { Heading as BaseHeading } from "@react-email/components" + +type HeadingProps = React.PropsWithChildren + +export const Heading = ({ children }: HeadingProps) => ( + + {children} + +) diff --git a/packages/react-email/ui/Layout.tsx b/packages/react-email/ui/Layout.tsx new file mode 100644 index 00000000..a4cc1fe9 --- /dev/null +++ b/packages/react-email/ui/Layout.tsx @@ -0,0 +1,85 @@ +import { + Body, + Head, + Hr, + Html, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components" + +type LayoutProps = React.PropsWithChildren & { + title: string + project_name: string + preview?: string +} + +export const Layout = ({ + project_name, + title, + preview, + children, +}: LayoutProps) => { + return ( + + + + {title} + + + {preview ? {preview} : null} + +
+
+ + {project_name} + +
+
{children}
+
+
+ + © {new Date().getFullYear()} {project_name}. All rights reserved. + +
+ + +
+ ) +} diff --git a/packages/react-email/ui/Link.tsx b/packages/react-email/ui/Link.tsx new file mode 100644 index 00000000..b2011df8 --- /dev/null +++ b/packages/react-email/ui/Link.tsx @@ -0,0 +1,7 @@ +type LinkProps = React.PropsWithChildren & { href: string } + +export const Link = ({ children, href }: LinkProps) => ( + + {children} + +) From f2035ef7329f43d19921119845073160754579e8 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:59:38 +0000 Subject: [PATCH 40/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 30d56f14..a070b1fa 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ ### Features +* ✨ Add react-email for email templates. PR [#2421](https://github.com/fastapi/full-stack-fastapi-template/pull/2421) by [@alejsdev](https://github.com/alejsdev). * 🔧 Use FastAPI entrypoint. PR [#2360](https://github.com/fastapi/full-stack-fastapi-template/pull/2360) by [@tiangolo](https://github.com/tiangolo). * ♻️ Add library-skills for FastAPI and SQLModel. PR [#2354](https://github.com/fastapi/full-stack-fastapi-template/pull/2354) by [@tiangolo](https://github.com/tiangolo). From 5069df32eafd5c0603b7eaf44b35f935d19295cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 20:01:12 +0200 Subject: [PATCH 41/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20FastAPI=20entr?= =?UTF-8?q?ypoint=20in=20Compose=20(#2420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- compose.override.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.override.yml b/compose.override.yml index 2206fe26..8ae8ad3c 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -44,9 +44,9 @@ services: # command: sleep infinity # Infinite loop to keep container alive doing nothing command: - fastapi - - run - - --reload - - "app/main.py" + - dev + - --host + - "0.0.0.0" develop: watch: - path: ./backend From 85cd14d474b02874cdf6486149f574adcd3af226 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:01:15 +0000 Subject: [PATCH 42/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index a070b1fa..2026fdac 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Refactors +* ♻️ Use FastAPI entrypoint in Compose. PR [#2420](https://github.com/fastapi/full-stack-fastapi-template/pull/2420) by [@tiangolo](https://github.com/tiangolo). * ♻️ Require non-empty Compose configuration. PR [#2418](https://github.com/fastapi/full-stack-fastapi-template/pull/2418) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify optional environment configuration. PR [#2417](https://github.com/fastapi/full-stack-fastapi-template/pull/2417) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify Copier environment configuration. PR [#2416](https://github.com/fastapi/full-stack-fastapi-template/pull/2416) by [@tiangolo](https://github.com/tiangolo). From 4256e83fec38194d1bac8702cbc6254c630183fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 22:38:15 +0200 Subject: [PATCH 43/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20uv=20work?= =?UTF-8?q?space=20setup=20(#2423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- backend/README.md | 16 +--------------- compose.override.yml | 1 - frontend/README.md | 1 - 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/backend/README.md b/backend/README.md index 308aa6a8..2b5ffc6e 100644 --- a/backend/README.md +++ b/backend/README.md @@ -27,21 +27,7 @@ The API is available at `http://localhost:8000`, with automatic interactive docs ## General Workflow -By default, the dependencies are managed with [uv](https://docs.astral.sh/uv/), go there and install it. - -From `./backend/` you can install all the dependencies with: - -```console -$ uv sync -``` - -Then you can activate the virtual environment with: - -```console -$ source .venv/bin/activate -``` - -Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`. +Run backend commands from `./backend/` with `uv run`. Make sure your editor uses the Python interpreter at `.venv/bin/python` in the project root. Modify or add SQLModel models for data and SQL tables in `./backend/app/models.py`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud.py`. diff --git a/compose.override.yml b/compose.override.yml index 8ae8ad3c..4454aae0 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -53,7 +53,6 @@ services: action: sync target: /app/backend ignore: - - ./backend/.venv - .venv - path: ./backend/pyproject.toml action: rebuild diff --git a/frontend/README.md b/frontend/README.md index 6c34e820..3b048ce5 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -39,7 +39,6 @@ Done, you have a frontend-less (api-only) app. 🤓 ### Automatically -* Activate the backend virtual environment. * From the top level project directory, run the script: ```bash From 74cf9ed832890db2316ab9dd3e07ef52ee59e19f Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:38:18 +0000 Subject: [PATCH 44/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 2026fdac..06fa38eb 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Refactors +* ♻️ Simplify uv workspace setup. PR [#2423](https://github.com/fastapi/full-stack-fastapi-template/pull/2423) by [@tiangolo](https://github.com/tiangolo). * ♻️ Use FastAPI entrypoint in Compose. PR [#2420](https://github.com/fastapi/full-stack-fastapi-template/pull/2420) by [@tiangolo](https://github.com/tiangolo). * ♻️ Require non-empty Compose configuration. PR [#2418](https://github.com/fastapi/full-stack-fastapi-template/pull/2418) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify optional environment configuration. PR [#2417](https://github.com/fastapi/full-stack-fastapi-template/pull/2417) by [@tiangolo](https://github.com/tiangolo). From 23b0ba9bad6ee136b5ac55a25a04d298a2c3c553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 22:42:29 +0200 Subject: [PATCH 45/91] =?UTF-8?q?=F0=9F=91=B7=20Remove=20legacy=20label=20?= =?UTF-8?q?check=20(#2424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/labeler.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index f6a91dc7..bc990e96 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -5,9 +5,6 @@ on: - opened - synchronize - reopened - # For label-checker - - labeled - - unlabeled permissions: {} @@ -20,18 +17,4 @@ jobs: timeout-minutes: 5 steps: - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 - if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} - run: echo "Done adding labels" - # Run this after labeler applied labels - check-labels: - needs: - - labeler - permissions: - pull-requests: read - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: agilepathway/label-checker@c3d16ad512e7cea5961df85ff2486bb774caf3c5 # v1.6.65 - with: - one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal - repo_token: ${{ secrets.GITHUB_TOKEN }} From b5a664d510d2989d810af2f68627873de1d8aefe Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:42:32 +0000 Subject: [PATCH 46/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 06fa38eb..c15bd556 100644 --- a/release-notes.md +++ b/release-notes.md @@ -45,6 +45,7 @@ ### Internal +* 👷 Remove legacy label check. PR [#2424](https://github.com/fastapi/full-stack-fastapi-template/pull/2424) by [@tiangolo](https://github.com/tiangolo). * 🔐 Use PR Submit for pull requests. PR [#2411](https://github.com/fastapi/full-stack-fastapi-template/pull/2411) by [@tiangolo](https://github.com/tiangolo). * 👷 Use GitHub CLI for Git authentication. PR [#2410](https://github.com/fastapi/full-stack-fastapi-template/pull/2410) by [@tiangolo](https://github.com/tiangolo). * 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo). From 1c5dd7030835d93b7e8f793bad9e91b227316a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 22:52:32 +0200 Subject: [PATCH 47/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Require=20explicit?= =?UTF-8?q?=20SECRET=5FKEY=20(#2425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- backend/app/core/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index f3a344c0..273809e5 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,4 +1,3 @@ -import secrets import warnings from typing import Annotated, Any, Literal, Self @@ -30,7 +29,7 @@ class Settings(BaseSettings): extra="ignore", ) API_V1_STR: str = "/api/v1" - SECRET_KEY: str = secrets.token_urlsafe(32) + SECRET_KEY: str # 60 minutes * 24 hours * 8 days = 8 days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 FRONTEND_HOST: str = "http://localhost:5173" From 8893c6807ac8fa13f0797fa60ca478a75f02ca65 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:52:34 +0000 Subject: [PATCH 48/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index c15bd556..fb2b4906 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Refactors +* ♻️ Require explicit SECRET_KEY. PR [#2425](https://github.com/fastapi/full-stack-fastapi-template/pull/2425) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify uv workspace setup. PR [#2423](https://github.com/fastapi/full-stack-fastapi-template/pull/2423) by [@tiangolo](https://github.com/tiangolo). * ♻️ Use FastAPI entrypoint in Compose. PR [#2420](https://github.com/fastapi/full-stack-fastapi-template/pull/2420) by [@tiangolo](https://github.com/tiangolo). * ♻️ Require non-empty Compose configuration. PR [#2418](https://github.com/fastapi/full-stack-fastapi-template/pull/2418) by [@tiangolo](https://github.com/tiangolo). From 9d541082a3c1b3d529b40e7dc59857f15c5fc216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 23:09:54 +0200 Subject: [PATCH 49/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Run=20database=20set?= =?UTF-8?q?up=20explicitly=20before=20startup=20(#2426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .github/workflows/deploy-production.yml | 1 + .github/workflows/deploy-staging.yml | 1 + .github/workflows/playwright.yml | 1 + .github/workflows/test-docker-compose.yml | 1 + backend/README.md | 1 + compose.deploy.yml | 4 ---- compose.override.yml | 4 ---- compose.yml | 17 +---------------- deployment.md | 1 + development.md | 1 + frontend/README.md | 5 +++-- scripts/test-local.sh | 1 + scripts/test.sh | 1 + 13 files changed, 13 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 802bf8a3..e62fe953 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -33,4 +33,5 @@ jobs: with: persist-credentials: false - run: docker compose -f compose.yml -f compose.deploy.yml build + - run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh - run: docker compose -f compose.yml -f compose.deploy.yml up -d diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 561afddd..1c35943d 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -33,4 +33,5 @@ jobs: with: persist-credentials: false - run: docker compose -f compose.yml -f compose.deploy.yml build + - run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh - run: docker compose -f compose.yml -f compose.deploy.yml up -d diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 4c4f9019..75f6b591 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -78,6 +78,7 @@ jobs: - run: bash scripts/generate-client.sh - run: docker compose build - run: docker compose down -v --remove-orphans + - run: docker compose run --rm backend bash scripts/prestart.sh - name: Run Playwright tests run: docker compose run --rm playwright bunx playwright test --fail-on-flaky-tests --trace=retain-on-failure --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - run: docker compose down -v --remove-orphans diff --git a/.github/workflows/test-docker-compose.yml b/.github/workflows/test-docker-compose.yml index c36ea63f..4551a157 100644 --- a/.github/workflows/test-docker-compose.yml +++ b/.github/workflows/test-docker-compose.yml @@ -20,6 +20,7 @@ jobs: persist-credentials: false - run: docker compose build - run: docker compose down -v --remove-orphans + - run: docker compose run --rm backend bash scripts/prestart.sh - run: docker compose up -d --wait backend adminer - name: Test backend is up run: curl http://localhost:8000/api/v1/utils/health-check diff --git a/backend/README.md b/backend/README.md index 2b5ffc6e..f4536194 100644 --- a/backend/README.md +++ b/backend/README.md @@ -42,6 +42,7 @@ The setup is also already configured so you can run the tests through the VS Cod To run the backend and built frontend in Docker Compose: ```console +$ docker compose run --rm backend bash scripts/prestart.sh $ docker compose watch ``` diff --git a/compose.deploy.yml b/compose.deploy.yml index 6ee6c9b0..3d810f51 100644 --- a/compose.deploy.yml +++ b/compose.deploy.yml @@ -44,10 +44,6 @@ services: # Use the Let's Encrypt resolver - traefik.http.routers.adminer-https.tls.certresolver=le - prestart: - environment: - FRONTEND_HOST: https://${DOMAIN:?Variable not set} - backend: restart: always environment: diff --git a/compose.override.yml b/compose.override.yml index 4454aae0..a11e3935 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -72,10 +72,6 @@ services: SMTP_PORT: "1025" SMTP_TLS: "false" - prestart: - environment: - ENVIRONMENT: "local" - mailcatcher: image: schickling/mailcatcher ports: diff --git a/compose.yml b/compose.yml index 0d164713..b955333c 100644 --- a/compose.yml +++ b/compose.yml @@ -53,10 +53,7 @@ services: db: condition: service_healthy restart: true - prestart: - condition: service_completed_successfully - # The YAML anchor lets the prestart service reuse this environment mapping. - environment: &backend-environment + environment: PROJECT_NAME: ${PROJECT_NAME:?Variable not set} BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS:-} SECRET_KEY: ${SECRET_KEY:?Variable not set} @@ -92,17 +89,5 @@ services: - traefik.http.routers.backend-http.rule=Host(`${DOMAIN:-localhost}`) - traefik.http.routers.backend-http.entrypoints=http - prestart: - image: backend:latest - build: - context: . - dockerfile: backend/Dockerfile - depends_on: - db: - condition: service_healthy - restart: true - command: bash scripts/prestart.sh - environment: *backend-environment - volumes: app-db-data: diff --git a/deployment.md b/deployment.md index dcb9d66e..3c2c4083 100644 --- a/deployment.md +++ b/deployment.md @@ -99,6 +99,7 @@ With the environment variables in place, you can deploy with Docker Compose: ```bash cd /root/code/app/ docker compose -f compose.yml -f compose.deploy.yml build +docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh docker compose -f compose.yml -f compose.deploy.yml up -d ``` diff --git a/development.md b/development.md index 3a537b2d..e769a106 100644 --- a/development.md +++ b/development.md @@ -57,6 +57,7 @@ The build is written to `backend/app/frontend` and served by FastAPI at Date: Mon, 10 Aug 2026 21:09:56 +0000 Subject: [PATCH 50/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index fb2b4906..a9dcfafa 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Refactors +* ♻️ Run database setup explicitly before startup, no need for prestart. PR [#2426](https://github.com/fastapi/full-stack-fastapi-template/pull/2426) by [@tiangolo](https://github.com/tiangolo). * ♻️ Require explicit SECRET_KEY. PR [#2425](https://github.com/fastapi/full-stack-fastapi-template/pull/2425) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify uv workspace setup. PR [#2423](https://github.com/fastapi/full-stack-fastapi-template/pull/2423) by [@tiangolo](https://github.com/tiangolo). * ♻️ Use FastAPI entrypoint in Compose. PR [#2420](https://github.com/fastapi/full-stack-fastapi-template/pull/2420) by [@tiangolo](https://github.com/tiangolo). From e95e501951a0ba2357a9566fae4318a7dd39e8a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 23:21:59 +0200 Subject: [PATCH 51/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20FASTAPI=5FENV?= =?UTF-8?q?=20for=20development=20(#2427)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- .env | 4 ++-- backend/app/api/main.py | 2 +- backend/app/core/config.py | 4 ++-- backend/app/main.py | 2 +- compose.override.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.env b/.env index f6c896a4..1f8e41b1 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ -# Enable local-only development behavior -ENVIRONMENT=local +# Enable development behavior for commands that import the app directly +FASTAPI_ENV=development PROJECT_NAME="Full Stack FastAPI Project" diff --git a/backend/app/api/main.py b/backend/app/api/main.py index eac18c8e..a42e5003 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -10,5 +10,5 @@ api_router.include_router(utils.router) api_router.include_router(items.router) -if settings.ENVIRONMENT == "local": +if settings.FASTAPI_ENV == "development": api_router.include_router(private.router) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 273809e5..7c8cea57 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -33,7 +33,7 @@ class Settings(BaseSettings): # 60 minutes * 24 hours * 8 days = 8 days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 FRONTEND_HOST: str = "http://localhost:5173" - ENVIRONMENT: Literal["local"] | None = None + FASTAPI_ENV: Literal["development"] | None = None BACKEND_CORS_ORIGINS: Annotated[ list[AnyUrl] | str, BeforeValidator(parse_cors) @@ -98,7 +98,7 @@ class Settings(BaseSettings): f'The value of {var_name} is "changethis", ' "for security, please change it, at least for deployments." ) - if self.ENVIRONMENT == "local": + if self.FASTAPI_ENV == "development": warnings.warn(message, stacklevel=1) else: raise ValueError(message) diff --git a/backend/app/main.py b/backend/app/main.py index d0cb480f..6da8e77f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -15,7 +15,7 @@ def custom_generate_unique_id(route: APIRoute) -> str: return f"{route.tags[0]}-{route.name}" -if settings.SENTRY_DSN and settings.ENVIRONMENT != "local": +if settings.SENTRY_DSN and settings.FASTAPI_ENV != "development": sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True) app = FastAPI( diff --git a/compose.override.yml b/compose.override.yml index a11e3935..ba3a1b4e 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -67,7 +67,7 @@ services: volumes: - ./backend/htmlcov:/app/backend/htmlcov environment: - ENVIRONMENT: "local" + FASTAPI_ENV: "development" SMTP_HOST: "mailcatcher" SMTP_PORT: "1025" SMTP_TLS: "false" From b1abd91ba708d64f2e31bde388945f23c883d8e8 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:22:02 +0000 Subject: [PATCH 52/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index a9dcfafa..f48ad42b 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Refactors +* ♻️ Use `FASTAPI_ENV` for development. PR [#2427](https://github.com/fastapi/full-stack-fastapi-template/pull/2427) by [@tiangolo](https://github.com/tiangolo). * ♻️ Run database setup explicitly before startup, no need for prestart. PR [#2426](https://github.com/fastapi/full-stack-fastapi-template/pull/2426) by [@tiangolo](https://github.com/tiangolo). * ♻️ Require explicit SECRET_KEY. PR [#2425](https://github.com/fastapi/full-stack-fastapi-template/pull/2425) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify uv workspace setup. PR [#2423](https://github.com/fastapi/full-stack-fastapi-template/pull/2423) by [@tiangolo](https://github.com/tiangolo). From c291285975cfacff15689f1800a45408652fe5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 23:33:16 +0200 Subject: [PATCH 53/91] =?UTF-8?q?=F0=9F=93=9D=20Remove=20obsolete=20local?= =?UTF-8?q?=20domain=20setup=20(#2428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- compose.override.yml | 4 ---- development.md | 48 +------------------------------------------- 2 files changed, 1 insertion(+), 51 deletions(-) diff --git a/compose.override.yml b/compose.override.yml index ba3a1b4e..05b98090 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -1,9 +1,5 @@ services: - # Local services are available on their ports, but also available on: - # http://localhost.tiangolo.com: application - # etc. To enable it, update .env, set: - # DOMAIN=localhost.tiangolo.com proxy: image: traefik:3.6 ports: diff --git a/development.md b/development.md index e769a106..198a5ca3 100644 --- a/development.md +++ b/development.md @@ -75,40 +75,12 @@ Mailcatcher: Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`. -**Note**: The first time you start the stack, it might take a minute to be ready while the backend waits for the database and configures everything. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service. +**Note**: The first time you start the stack, it might take a minute for all the services to be ready. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service. ## Mailcatcher Mailcatcher captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailcatcher` service. Captured emails are available at . -## Docker Compose in `localhost.tiangolo.com` - -When you start the Docker Compose stack, it uses `localhost` by default, with different ports for each service (backend, adminer, etc). - -When you deploy it to production (or staging), the application uses one domain. The frontend is served at `/` and the API lives under `/api`. - -In the guide about [deployment](deployment.md) you can read about Traefik, the configured proxy. That's the component in charge of transmitting traffic to the application service based on the domain. - -If you want to test that it's all working locally, you can edit the local `.env` file, and change: - -```dotenv -DOMAIN=localhost.tiangolo.com -``` - -That will be used by the Docker Compose files to configure the base domain for the services. - -Traefik will transmit application traffic at `localhost.tiangolo.com` to FastAPI, which serves both the frontend and API. - -The domain `localhost.tiangolo.com` is a special domain that is configured (with all its subdomains) to point to `127.0.0.1`. This way you can use that for your local development. - -After you update it, run again: - -```bash -docker compose watch -``` - -Traefik is included in the main Docker Compose file. The development overrides expose its local dashboard so you can inspect the routes used with `localhost.tiangolo.com`. - ## Docker Compose files and env vars There is a main `compose.yml` file with all the configurations that apply to the whole stack, it is used automatically by `docker compose`. @@ -119,8 +91,6 @@ The `compose.deploy.yml` file contains the deployment-specific settings, includi The backend reads local settings from the `.env` file. Docker Compose also uses it for variable interpolation and passes the settings each container needs. -They also use some additional configurations taken from environment variables set in the scripts before calling the `docker compose` command. - After changing variables, make sure you restart the stack: ```bash @@ -203,19 +173,3 @@ Adminer: Traefik UI: MailCatcher: - -### Development URLs with `localhost.tiangolo.com` Configured - -Development URLs, for local development. - -Application: - -Automatic Interactive Docs (Swagger UI): - -Automatic Alternative Docs (ReDoc): - -Adminer: - -Traefik UI: - -MailCatcher: From 781b283849e602fa2c2c3562b2fe4fbd9374919d Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:33:19 +0000 Subject: [PATCH 54/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index f48ad42b..a62fd0f6 100644 --- a/release-notes.md +++ b/release-notes.md @@ -40,6 +40,7 @@ ### Docs +* 📝 Remove obsolete local domain setup. PR [#2428](https://github.com/fastapi/full-stack-fastapi-template/pull/2428) by [@tiangolo](https://github.com/tiangolo). * 📝 Clarify environment configuration. PR [#2419](https://github.com/fastapi/full-stack-fastapi-template/pull/2419) by [@tiangolo](https://github.com/tiangolo). * 📝 Improve README screenshot alt text. PR [#2359](https://github.com/fastapi/full-stack-fastapi-template/pull/2359) by [@Bmowville](https://github.com/Bmowville). * ✏️ Fix typo in the comment for DOMAIN in the .env file. PR [#2305](https://github.com/fastapi/full-stack-fastapi-template/pull/2305) by [@serhiiur](https://github.com/serhiiur). From d85803bf642102ecbc01ea676501106ae12a5e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 10 Aug 2026 23:42:37 +0200 Subject: [PATCH 55/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20CORS=20co?= =?UTF-8?q?nfiguration=20(#2429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## AI Disclaimer Using Codex with gpt-5.6-sol. Reviewed manually. --- backend/app/core/config.py | 23 +---------------------- backend/app/main.py | 16 +++++++--------- compose.yml | 1 - deployment.md | 1 - 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 7c8cea57..75c9fae2 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,9 +1,7 @@ import warnings -from typing import Annotated, Any, Literal, Self +from typing import Literal, Self from pydantic import ( - AnyUrl, - BeforeValidator, EmailStr, HttpUrl, PostgresDsn, @@ -13,14 +11,6 @@ from pydantic import ( from pydantic_settings import BaseSettings, SettingsConfigDict -def parse_cors(v: Any) -> list[str] | str: - if isinstance(v, str) and not v.startswith("["): - return [i.strip() for i in v.split(",") if i.strip()] - elif isinstance(v, list | str): - return v - raise ValueError(v) - - class Settings(BaseSettings): model_config = SettingsConfigDict( # Use top level .env file (one level above ./backend/) @@ -35,17 +25,6 @@ class Settings(BaseSettings): FRONTEND_HOST: str = "http://localhost:5173" FASTAPI_ENV: Literal["development"] | None = None - BACKEND_CORS_ORIGINS: Annotated[ - list[AnyUrl] | str, BeforeValidator(parse_cors) - ] = [] - - @computed_field # type: ignore[prop-decorator] - @property - def all_cors_origins(self) -> list[str]: - return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ - self.FRONTEND_HOST - ] - PROJECT_NAME: str SENTRY_DSN: HttpUrl | None = None POSTGRES_SERVER: str diff --git a/backend/app/main.py b/backend/app/main.py index 6da8e77f..f2352b70 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -24,15 +24,13 @@ app = FastAPI( generate_unique_id_function=custom_generate_unique_id, ) -# Set all CORS enabled origins -if settings.all_cors_origins: - app.add_middleware( - CORSMiddleware, - allow_origins=settings.all_cors_origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) +app.add_middleware( + CORSMiddleware, + allow_origins=[settings.FRONTEND_HOST], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) app.include_router(api_router, prefix=settings.API_V1_STR) app.frontend("/", directory=FRONTEND_DIR) diff --git a/compose.yml b/compose.yml index b955333c..b785dd96 100644 --- a/compose.yml +++ b/compose.yml @@ -55,7 +55,6 @@ services: restart: true environment: PROJECT_NAME: ${PROJECT_NAME:?Variable not set} - BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS:-} SECRET_KEY: ${SECRET_KEY:?Variable not set} FIRST_SUPERUSER: ${FIRST_SUPERUSER:?Variable not set} FIRST_SUPERUSER_PASSWORD: ${FIRST_SUPERUSER_PASSWORD:?Variable not set} diff --git a/deployment.md b/deployment.md index 3c2c4083..e74fde16 100644 --- a/deployment.md +++ b/deployment.md @@ -75,7 +75,6 @@ export FIRST_SUPERUSER_PASSWORD="$(python -c 'import secrets; print(secrets.toke You can set several other environment variables: * `PROJECT_NAME`: The name of the project, used in the API for the docs and emails. -* `BACKEND_CORS_ORIGINS`: A list of additional allowed CORS origins separated by commas. The frontend served by FastAPI uses the same origin and doesn't need to be added. * `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users. * `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc). * `SMTP_USER`: The SMTP server user to send emails. From 9a218cd74ab55ff68c9ac7c07d7f4a1b450fca6a Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:42:39 +0000 Subject: [PATCH 56/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index a62fd0f6..d0214197 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Refactors +* ♻️ Simplify CORS configuration. PR [#2429](https://github.com/fastapi/full-stack-fastapi-template/pull/2429) by [@tiangolo](https://github.com/tiangolo). * ♻️ Use `FASTAPI_ENV` for development. PR [#2427](https://github.com/fastapi/full-stack-fastapi-template/pull/2427) by [@tiangolo](https://github.com/tiangolo). * ♻️ Run database setup explicitly before startup, no need for prestart. PR [#2426](https://github.com/fastapi/full-stack-fastapi-template/pull/2426) by [@tiangolo](https://github.com/tiangolo). * ♻️ Require explicit SECRET_KEY. PR [#2425](https://github.com/fastapi/full-stack-fastapi-template/pull/2425) by [@tiangolo](https://github.com/tiangolo). From af791805aceab6e74db98edcc27b6f26722f326b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 11 Aug 2026 00:01:36 +0200 Subject: [PATCH 57/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20Copier=20pr?= =?UTF-8?q?oject=20generation=20(#2430)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .copier/.copier-answers.yml.jinja | 1 - .copier/update_dotenv.py | 37 --------------- README.md | 55 ----------------------- copier.yml | 75 ------------------------------- 4 files changed, 168 deletions(-) delete mode 100644 .copier/.copier-answers.yml.jinja delete mode 100644 .copier/update_dotenv.py delete mode 100644 copier.yml diff --git a/.copier/.copier-answers.yml.jinja b/.copier/.copier-answers.yml.jinja deleted file mode 100644 index 0028a239..00000000 --- a/.copier/.copier-answers.yml.jinja +++ /dev/null @@ -1 +0,0 @@ -{{ _copier_answers|to_json -}} diff --git a/.copier/update_dotenv.py b/.copier/update_dotenv.py deleted file mode 100644 index ceb8eb22..00000000 --- a/.copier/update_dotenv.py +++ /dev/null @@ -1,37 +0,0 @@ -from pathlib import Path -import json - -# Update the .env file with the answers from the .copier-answers.yml file -# without using Jinja2 templates in the .env file, this way the code works as is -# without needing Copier, but if Copier is used, the .env file will be updated -root_path = Path(__file__).parent.parent -answers_path = Path(__file__).parent / ".copier-answers.yml" -answers = json.loads(answers_path.read_text()) -env_path = root_path / ".env" -env_content = env_path.read_text() -# Keep deployment provider settings from replacing local development defaults. -env_answer_keys = { - "emails_from_email", - "first_superuser", - "first_superuser_password", - "postgres_password", - "project_name", - "secret_key", -} -lines = [] -for line in env_content.splitlines(): - for key, value in answers.items(): - if key not in env_answer_keys: - continue - upper_key = key.upper() - if line.startswith(f"{upper_key}="): - if " " in value: - content = f"{upper_key}={value!r}" - else: - content = f"{upper_key}={value}" - new_line = line.replace(line, content) - lines.append(new_line) - break - else: - lines.append(line) -env_path.write_text("\n".join(lines)) diff --git a/README.md b/README.md index 00fd3894..010e8fc9 100644 --- a/README.md +++ b/README.md @@ -147,61 +147,6 @@ python -c "import secrets; print(secrets.token_urlsafe(32))" Copy the content and use that as password / secret key. And run that again to generate another secure key. -## How To Use It - Alternative With Copier - -This repository also supports generating a new project using [Copier](https://copier.readthedocs.io). - -It will copy all the files, ask you configuration questions, and update the `.env` files with your answers. - -### Install Copier - -You can install Copier with: - -```bash -pip install copier -``` - -Or better, if you have [`pipx`](https://pipx.pypa.io/), you can run it with: - -```bash -pipx install copier -``` - -**Note**: If you have `pipx`, installing copier is optional, you could run it directly. - -### Generate a Project With Copier - -Decide a name for your new project's directory, you will use it below. For example, `my-awesome-project`. - -Go to the directory that will be the parent of your project, and run the command with your project's name: - -```bash -copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust -``` - -If you have `pipx` and you didn't install `copier`, you can run it directly: - -```bash -pipx run copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust -``` - -**Note** the `--trust` option is necessary to be able to execute a [post-creation script](https://github.com/fastapi/full-stack-fastapi-template/blob/master/.copier/update_dotenv.py) that updates your `.env` files. - -### Input Variables - -Copier will ask you for some data, you might want to have at hand before generating the project. - -But don't worry, you can just update any of that in the `.env` files afterwards. - -The input variables, with their default values (some auto generated) are: - -- `project_name`: (default: `"FastAPI Project"`) The name of the project, shown to API users (in .env). -- `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above. -- `first_superuser`: (default: `"admin@example.com"`) The email of the first superuser (in .env). -- `first_superuser_password`: (default: `"changethis"`) The password of the first superuser (in .env). -- `emails_from_email`: (default: `"info@example.com"`) The email account to send emails from, you can set it later in .env. -- `postgres_password`: (default: `"changethis"`) The password for the PostgreSQL database, stored in .env, you can generate one with the method above. - ## Backend Development Backend docs: [backend/README.md](./backend/README.md). diff --git a/copier.yml b/copier.yml deleted file mode 100644 index 6d6c99a8..00000000 --- a/copier.yml +++ /dev/null @@ -1,75 +0,0 @@ -project_name: - type: str - help: The name of the project, shown to API users (in .env) - default: FastAPI Project - -secret_key: - type: str - help: | - 'The secret key for the project, used for security, - stored in .env, you can generate one with: - python -c "import secrets; print(secrets.token_urlsafe(32))"' - default: changethis - -first_superuser: - type: str - help: The email of the first superuser (in .env) - default: admin@example.com - -first_superuser_password: - type: str - help: The password of the first superuser (in .env) - default: changethis - -emails_from_email: - type: str - help: The email account to send emails from, you can set it later in .env - default: info@example.com - -postgres_password: - type: str - help: | - 'The password for the PostgreSQL database, stored in .env, - you can generate one with: - python -c "import secrets; print(secrets.token_urlsafe(32))"' - default: changethis - -_exclude: - # Global - - .vscode - - .mypy_cache - # Python - - __pycache__ - - app.egg-info - - "*.pyc" - - .mypy_cache - - .coverage - - htmlcov - - .cache - - .venv - # Frontend - # Logs - - logs - - "*.log" - - npm-debug.log* - - yarn-debug.log* - - yarn-error.log* - - pnpm-debug.log* - - lerna-debug.log* - - node_modules - - dist - - dist-ssr - - "*.local" - # Editor directories and files - - .idea - - .DS_Store - - "*.suo" - - "*.ntvs*" - - "*.njsproj" - - "*.sln" - - "*.sw?" - -_answers_file: .copier/.copier-answers.yml - -_tasks: - - ["{{ _copier_python }}", .copier/update_dotenv.py] From fd0ca44d77853d4af50b47c16f288fd3085a84ef Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:01:38 +0000 Subject: [PATCH 58/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index d0214197..e8c06dbe 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Refactors +* ♻️ Remove Copier project generation. PR [#2430](https://github.com/fastapi/full-stack-fastapi-template/pull/2430) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify CORS configuration. PR [#2429](https://github.com/fastapi/full-stack-fastapi-template/pull/2429) by [@tiangolo](https://github.com/tiangolo). * ♻️ Use `FASTAPI_ENV` for development. PR [#2427](https://github.com/fastapi/full-stack-fastapi-template/pull/2427) by [@tiangolo](https://github.com/tiangolo). * ♻️ Run database setup explicitly before startup, no need for prestart. PR [#2426](https://github.com/fastapi/full-stack-fastapi-template/pull/2426) by [@tiangolo](https://github.com/tiangolo). From 512e496e1f43f4628e92a43ad9ca0d351dbd7773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 11 Aug 2026 10:53:23 +0200 Subject: [PATCH 59/91] =?UTF-8?q?=F0=9F=91=B7=20Add=20release=20automation?= =?UTF-8?q?=20(#2431)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pr-submit.yml | 1 + .github/workflows/create-draft-release.yml | 54 +++++++ .github/workflows/prepare-release.yml | 84 ++++++++++ deployment.md | 8 + pyproject.toml | 1 + scripts/prepare_release.py | 175 +++++++++++++++++++++ uv.lock | 9 ++ 7 files changed, 332 insertions(+) create mode 100644 .github/workflows/create-draft-release.yml create mode 100644 .github/workflows/prepare-release.yml create mode 100644 scripts/prepare_release.py diff --git a/.github/pr-submit.yml b/.github/pr-submit.yml index 1c5d0aa4..c9aa5241 100644 --- a/.github/pr-submit.yml +++ b/.github/pr-submit.yml @@ -1,2 +1,3 @@ workflows: - .github/workflows/bump-pre-commit-hooks.yml + - .github/workflows/prepare-release.yml diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml new file mode 100644 index 00000000..da68af95 --- /dev/null +++ b/.github/workflows/create-draft-release.yml @@ -0,0 +1,54 @@ +name: Create Draft Release + +on: + pull_request: + types: + - closed + +permissions: {} + +jobs: + create-draft-release: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + pull-requests: read + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version-file: ".python-version" + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. + # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 + version: "0.11.18" + - name: Extract release details + id: release-details + run: | + set -euo pipefail + version="$(uv run python scripts/prepare_release.py current-version)" + uv run python scripts/prepare_release.py release-notes > draft-release-notes.md + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Create draft release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.release-details.outputs.version }} + run: | + set -euo pipefail + gh release create "$VERSION" \ + --draft \ + --title "$VERSION" \ + --notes-file draft-release-notes.md \ + --target "$(git rev-parse HEAD)" diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..fa0e033b --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,84 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + bump: + description: Release bump + required: true + type: choice + options: + - patch + - minor + - major + date: + description: Release date in YYYY-MM-DD format. Defaults to today. + required: false + type: string + +permissions: {} + +jobs: + prepare-release: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + id-token: write + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version-file: ".python-version" + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. + # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 + version: "0.11.18" + - name: Prepare release + env: + BUMP: ${{ inputs.bump }} + RELEASE_DATE: ${{ inputs.date }} + run: uv run python scripts/prepare_release.py prepare "$BUMP" --date "$RELEASE_DATE" + - name: Get release version + id: release-version + run: | + version="$(uv run python scripts/prepare_release.py current-version)" + echo "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Get PR Submit token + id: pr-submit + uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1 + - name: Create release pull request + env: + BASE_BRANCH: ${{ github.event.repository.default_branch }} + GH_TOKEN: ${{ steps.pr-submit.outputs.token }} + VERSION: ${{ steps.release-version.outputs.version }} + run: | + set -euo pipefail + branch="release-${VERSION}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + git config user.name "pr-submit[bot]" + git config user.email "pr-submit[bot]@users.noreply.github.com" + git switch -c "$branch" + git add release-notes.md + git commit -m "🔖 Release version ${VERSION}" + gh auth setup-git + git push --set-upstream origin "$branch" + gh label create release \ + --description "Release preparation" \ + --color ededed \ + --force + gh pr create \ + --base "$BASE_BRANCH" \ + --head "$branch" \ + --title "🔖 Release version ${VERSION}" \ + --body "Prepare release ${VERSION}." \ + --label release diff --git a/deployment.md b/deployment.md index e74fde16..83be48ad 100644 --- a/deployment.md +++ b/deployment.md @@ -218,6 +218,14 @@ There are GitHub Action workflows in the `.github/workflows` directory already c Both workflows are associated with their respective GitHub Environments, so deployments will be visible in the repository's **Environments** section and will respect any protection rules you configure. +### Prepare a Release + +Install the [PR Submit GitHub App](https://github.com/apps/pr-submit) in your repository to enable the **Prepare Release** workflow. + +Run the workflow manually from the **Actions** tab and select the version bump. It creates a pull request that updates the release notes. When you merge that pull request, the **Create Draft Release** workflow creates a draft GitHub release with the corresponding release notes. + +Review and publish the draft release to trigger the production deployment. + If you need to add extra environments you could use those as a starting point. ## URLs diff --git a/pyproject.toml b/pyproject.toml index c7eed2e8..a593c696 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [dependency-groups] dev = [ "prek>=0.4.11,<1.0.0", + "typer>=0.20.0,<1.0.0", "zizmor>=1.28.0", ] diff --git a/scripts/prepare_release.py b/scripts/prepare_release.py new file mode 100644 index 00000000..c0106bee --- /dev/null +++ b/scripts/prepare_release.py @@ -0,0 +1,175 @@ +"""Prepare a release by updating and extracting release notes.""" + +import re +from datetime import date +from pathlib import Path +from typing import Annotated, Literal + +import typer + +RELEASE_NOTES_FILE = Path("release-notes.md") +RELEASE_NOTES_HEADER = "# Release Notes\n\n" +LATEST_CHANGES_HEADER = "## Latest Changes" +VERSION_HEADING_PATTERN = re.compile(r"(?m)^## (\d+\.\d+\.\d+)(?: \([^)]+\))?$") +BumpType = Literal["major", "minor", "patch"] + +app = typer.Typer() + + +def parse_version(version: str) -> tuple[int, int, int]: + if not re.fullmatch(r"\d+\.\d+\.\d+", version): + raise ValueError(f"Invalid version: {version!r}. Expected format: X.Y.Z") + major, minor, patch = version.split(".") + return int(major), int(minor), int(patch) + + +def get_current_version(content: str, release_notes_file: Path) -> str: + match = VERSION_HEADING_PATTERN.search(content) + if not match: + raise RuntimeError(f"Could not find a version section in {release_notes_file}") + return match.group(1) + + +def bump_version(version: str, bump: BumpType) -> str: + major, minor, patch = parse_version(version) + if bump == "major": + return f"{major + 1}.0.0" + if bump == "minor": + return f"{major}.{minor + 1}.0" + return f"{major}.{minor}.{patch + 1}" + + +def update_release_notes( + content: str, version: str, release_date: date, release_notes_file: Path +) -> str: + latest_header = f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n" + if not content.startswith(latest_header): + raise RuntimeError(f"{release_notes_file} must start with {latest_header!r}") + if re.search(rf"^## {re.escape(version)}(?: \([^)]+\))?$", content, re.M): + raise RuntimeError(f"Release notes already contain a section for {version}") + + current_version = get_current_version(content, release_notes_file) + if parse_version(version) <= parse_version(current_version): + raise RuntimeError( + f"New version {version} must be greater than current version " + f"{current_version}" + ) + + current_match = VERSION_HEADING_PATTERN.search(content) + assert current_match is not None + latest_changes = content[len(latest_header) : current_match.start()].strip() + if not latest_changes: + raise RuntimeError("The Latest Changes section is empty") + + release_header = f"## {version} ({release_date.isoformat()})" + return content.replace( + latest_header, + f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n\n{release_header}\n", + 1, + ) + + +def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str: + version_heading = re.compile(rf"(?m)^## {re.escape(version)}(?: \([^)]+\))?$") + match = version_heading.search(content) + if not match: + raise RuntimeError( + f"Could not find release notes section for {version} in " + f"{release_notes_file}" + ) + + next_match = VERSION_HEADING_PATTERN.search(content, match.end()) + end = next_match.start() if next_match else len(content) + body = content[match.end() : end].strip() + if not body: + raise RuntimeError( + f"Release notes section for {version} in {release_notes_file} is empty" + ) + return f"{body}\n" + + +def prepare_release( + bump: BumpType, release_date: date, release_notes_file: Path +) -> str: + content = release_notes_file.read_text(encoding="utf-8") + version = bump_version(get_current_version(content, release_notes_file), bump) + release_notes_file.write_text( + update_release_notes(content, version, release_date, release_notes_file), + encoding="utf-8", + ) + return version + + +@app.command() +def prepare( + bump: Annotated[ + BumpType, + typer.Argument(help="The release bump to make: major, minor, or patch."), + ], + release_notes_file: Annotated[ + Path, + typer.Option( + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + writable=True, + help="Path to the release notes Markdown file.", + ), + ] = RELEASE_NOTES_FILE, + release_date: Annotated[ + str, + typer.Option( + "--date", + help="Release date in YYYY-MM-DD format. Defaults to today.", + ), + ] = date.today().isoformat(), +) -> None: + parsed_release_date = date.fromisoformat(release_date or date.today().isoformat()) + version = prepare_release(bump, parsed_release_date, release_notes_file) + typer.echo(f"Prepared release {version} ({parsed_release_date.isoformat()})") + + +@app.command() +def current_version( + release_notes_file: Annotated[ + Path, + typer.Option( + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + help="Path to the release notes Markdown file.", + ), + ] = RELEASE_NOTES_FILE, +) -> None: + typer.echo( + get_current_version( + release_notes_file.read_text(encoding="utf-8"), release_notes_file + ) + ) + + +@app.command() +def release_notes( + release_notes_file: Annotated[ + Path, + typer.Option( + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + help="Path to the release notes Markdown file.", + ), + ] = RELEASE_NOTES_FILE, +) -> None: + content = release_notes_file.read_text(encoding="utf-8") + version = get_current_version(content, release_notes_file) + typer.echo( + get_release_notes_body(content, version, release_notes_file), + nl=False, + ) + + +if __name__ == "__main__": + app() diff --git a/uv.lock b/uv.lock index 8ce206e1..0107c9d2 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,7 @@ members = [ [manifest.dependency-groups] dev = [ { name = "prek", specifier = ">=0.4.11,<1.0.0" }, + { name = "typer", specifier = ">=0.20.0,<1.0.0" }, { name = "zizmor", specifier = ">=1.28.0" }, ] github-actions = [{ name = "smokeshow", specifier = ">=0.5.0" }] @@ -529,7 +530,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" }, { url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" }, { url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/2664d290cbd1fef9eb3f69b5d3bc5aa91b6fa907519298ca6af93a90c6cb/greenlet-3.5.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", size = 669989, upload-time = "2026-06-17T18:39:30.79Z" }, { url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" }, + { url = "https://files.pythonhosted.org/packages/42/d4/fcb53fa9847d7fbd4723fbed9469c3869b9e3544c4e001d9d5aa2f66162d/greenlet-3.5.2-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", size = 472888, upload-time = "2026-06-17T18:41:22.511Z" }, { url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" }, { url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" }, { url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" }, @@ -537,14 +540,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" }, { url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" }, { url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" }, + { url = "https://files.pythonhosted.org/packages/35/ad/9b3058f999b81750a9c6d9ec424f509462d232b58002086fe2ba63b66407/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", size = 658945, upload-time = "2026-06-17T18:39:32.509Z" }, { url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" }, + { url = "https://files.pythonhosted.org/packages/92/75/1b6ecd8c027b69ab1b6798a84094df79aab5e69ac7e249c78b9d361dd1fa/greenlet-3.5.2-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", size = 490529, upload-time = "2026-06-17T18:41:23.954Z" }, { url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" }, { url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" }, { url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" }, { url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" }, { url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" }, { url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/53/e0/4ce3a046b51e53934eae93d7f9c13975a97285741e9e1fcadf8751314c37/greenlet-3.5.2-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", size = 673494, upload-time = "2026-06-17T18:39:34.196Z" }, { url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/52/e0/9c18721e63445dce02ee67e4c81c0f281626604ff55ae6f7b7f4354d7129/greenlet-3.5.2-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", size = 479721, upload-time = "2026-06-17T18:41:25.726Z" }, { url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" }, { url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" }, { url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" }, @@ -552,7 +559,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" }, { url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/10776cd88df54d0f563e9e21e98363f2d6af94bedc553b1da0972fa87f80/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", size = 663191, upload-time = "2026-06-17T18:39:35.639Z" }, { url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/02/aa/26ddf92826a99d87bfb8fdb8f3a262a6f16495a5d8e579737baa92fb4543/greenlet-3.5.2-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", size = 498199, upload-time = "2026-06-17T18:41:27.464Z" }, { url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" }, { url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" }, { url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" }, From c8f90691682fff257963ad94d3e3f3285d5c0d44 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:53:26 +0000 Subject: [PATCH 60/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index e8c06dbe..7eeec49e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -51,6 +51,7 @@ ### Internal +* 👷 Add release automation. PR [#2431](https://github.com/fastapi/full-stack-fastapi-template/pull/2431) by [@tiangolo](https://github.com/tiangolo). * 👷 Remove legacy label check. PR [#2424](https://github.com/fastapi/full-stack-fastapi-template/pull/2424) by [@tiangolo](https://github.com/tiangolo). * 🔐 Use PR Submit for pull requests. PR [#2411](https://github.com/fastapi/full-stack-fastapi-template/pull/2411) by [@tiangolo](https://github.com/tiangolo). * 👷 Use GitHub CLI for Git authentication. PR [#2410](https://github.com/fastapi/full-stack-fastapi-template/pull/2410) by [@tiangolo](https://github.com/tiangolo). From cf1548a983fd0339dd1d0533b0db495614e27f7b Mon Sep 17 00:00:00 2001 From: "pr-submit[bot]" <314742883+pr-submit[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:29:32 +0000 Subject: [PATCH 61/91] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11.0?= =?UTF-8?q?=20(#2434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pr-submit[bot] --- release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-notes.md b/release-notes.md index 7eeec49e..8d3ec998 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.11.0 (2026-08-11) + ### Features * ✨ Add react-email for email templates. PR [#2421](https://github.com/fastapi/full-stack-fastapi-template/pull/2421) by [@alejsdev](https://github.com/alejsdev). From 32ab6dd022cf46a2bc2f901f746d684078843969 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 11 Aug 2026 18:28:34 +0100 Subject: [PATCH 62/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migrate=20to=20`DATA?= =?UTF-8?q?BASE=5FURL`=20instead=20of=20separate=20variables=20(#2184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .env | 4 +--- backend/app/alembic/env.py | 2 +- backend/app/core/config.py | 29 ++++++++++++----------------- backend/app/core/db.py | 2 +- compose.yml | 10 +++------- deployment.md | 2 -- 6 files changed, 18 insertions(+), 31 deletions(-) diff --git a/.env b/.env index 1f8e41b1..e1678422 100644 --- a/.env +++ b/.env @@ -14,7 +14,5 @@ SMTP_TLS=False SMTP_PORT=1025 # Postgres -POSTGRES_SERVER=localhost -POSTGRES_DB=app -POSTGRES_USER=postgres POSTGRES_PASSWORD=changethis +DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432/app diff --git a/backend/app/alembic/env.py b/backend/app/alembic/env.py index fb993cf4..919eebf5 100755 --- a/backend/app/alembic/env.py +++ b/backend/app/alembic/env.py @@ -31,7 +31,7 @@ target_metadata = SQLModel.metadata def get_url(): - return str(settings.SQLALCHEMY_DATABASE_URI) + return str(settings.DATABASE_URL) def run_migrations_offline(): diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 75c9fae2..1f3c2873 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -6,6 +6,7 @@ from pydantic import ( HttpUrl, PostgresDsn, computed_field, + field_validator, model_validator, ) from pydantic_settings import BaseSettings, SettingsConfigDict @@ -27,23 +28,16 @@ class Settings(BaseSettings): PROJECT_NAME: str SENTRY_DSN: HttpUrl | None = None - POSTGRES_SERVER: str - POSTGRES_PORT: int = 5432 - POSTGRES_USER: str - POSTGRES_PASSWORD: str = "" - POSTGRES_DB: str = "" + DATABASE_URL: PostgresDsn - @computed_field # type: ignore[prop-decorator] - @property - def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: - return PostgresDsn.build( - scheme="postgresql+psycopg", - username=self.POSTGRES_USER, - password=self.POSTGRES_PASSWORD, - host=self.POSTGRES_SERVER, - port=self.POSTGRES_PORT, - path=self.POSTGRES_DB, - ) + @field_validator("DATABASE_URL", mode="before") + @classmethod + def _use_psycopg_driver(cls, value: str | PostgresDsn) -> str: + database_url = str(value) + for scheme in ("postgres://", "postgresql://"): + if database_url.startswith(scheme): + return database_url.replace(scheme, "postgresql+psycopg://", 1) + return database_url SMTP_TLS: bool = True SMTP_SSL: bool = False @@ -85,7 +79,8 @@ class Settings(BaseSettings): @model_validator(mode="after") def _enforce_non_default_secrets(self) -> Self: self._check_default_secret("SECRET_KEY", self.SECRET_KEY) - self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD) + for host in self.DATABASE_URL.hosts(): + self._check_default_secret("DATABASE_URL password", host["password"]) self._check_default_secret( "FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD ) diff --git a/backend/app/core/db.py b/backend/app/core/db.py index ba991fb3..f19ac0d3 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -4,7 +4,7 @@ from app import crud from app.core.config import settings from app.models import User, UserCreate -engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) +engine = create_engine(str(settings.DATABASE_URL)) # make sure all SQLModel models are imported (app.models) before initializing DB diff --git a/compose.yml b/compose.yml index b785dd96..dd549712 100644 --- a/compose.yml +++ b/compose.yml @@ -20,7 +20,7 @@ services: db: image: postgres:18 healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + test: ["CMD-SHELL", "pg_isready -U postgres -d app"] interval: 10s retries: 5 start_period: 30s @@ -29,8 +29,7 @@ services: - app-db-data:/var/lib/postgresql environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Variable not set} - - POSTGRES_USER=${POSTGRES_USER:?Variable not set} - - POSTGRES_DB=${POSTGRES_DB:?Variable not set} + - POSTGRES_DB=app adminer: image: adminer @@ -62,10 +61,7 @@ services: SMTP_USER: ${SMTP_USER:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL} - POSTGRES_SERVER: db - POSTGRES_DB: ${POSTGRES_DB:?Variable not set} - POSTGRES_USER: ${POSTGRES_USER:?Variable not set} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Variable not set} + DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:?Variable not set}@db:5432/app SENTRY_DSN: ${SENTRY_DSN:-} healthcheck: diff --git a/deployment.md b/deployment.md index 83be48ad..331ccbac 100644 --- a/deployment.md +++ b/deployment.md @@ -80,8 +80,6 @@ You can set several other environment variables: * `SMTP_USER`: The SMTP server user to send emails. * `SMTP_PASSWORD`: The SMTP server password to send emails. * `EMAILS_FROM_EMAIL`: The email account to send emails from. -* `POSTGRES_USER`: The Postgres user, you can leave the default. -* `POSTGRES_DB`: The database name to use for this application. You can leave the default of `app`. * `SENTRY_DSN`: The DSN for Sentry, if you are using it. ## GitHub Actions Environment Variables From 6d75b2d1325999796018c626cf64ec5608e7ca7e Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:28:37 +0000 Subject: [PATCH 63/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 8d3ec998..9755243c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Refactors + +* ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91). + ## 0.11.0 (2026-08-11) ### Features From 6dbb2e82569437955fe40a8c75f8f669b0e326a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:08:23 +0200 Subject: [PATCH 64/91] =?UTF-8?q?=E2=AC=86=20Bump=20the=20npm-packages=20g?= =?UTF-8?q?roup=20across=201=20directory=20with=2033=20updates=20(#2433)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> Co-authored-by: Sebastián Ramírez Co-authored-by: Yurii Motov --- bun.lock | 504 +++++++++++++++--------------- frontend/Dockerfile.playwright | 2 +- frontend/biome.json | 18 +- frontend/package.json | 60 ++-- frontend/src/routeTree.gen.ts | 122 ++++---- frontend/tsconfig.json | 2 +- frontend/vite.config.ts | 2 +- packages/react-email/package.json | 10 +- 8 files changed, 370 insertions(+), 350 deletions(-) diff --git a/bun.lock b/bun.lock index a5cf0d5d..dea386f2 100644 --- a/bun.lock +++ b/bun.lock @@ -9,55 +9,55 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@hookform/resolvers": "^5.4.0", - "@radix-ui/react-avatar": "^1.1.12", - "@radix-ui/react-checkbox": "^1.3.4", - "@radix-ui/react-dialog": "^1.1.16", - "@radix-ui/react-dropdown-menu": "^2.1.17", - "@radix-ui/react-label": "^2.1.9", - "@radix-ui/react-radio-group": "^1.4.0", - "@radix-ui/react-scroll-area": "^1.2.11", - "@radix-ui/react-select": "^2.3.0", - "@radix-ui/react-separator": "^1.1.9", + "@hookform/resolvers": "^5.7.1", + "@radix-ui/react-avatar": "^1.2.6", + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-radio-group": "^1.4.7", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "^1.1.14", - "@radix-ui/react-tooltip": "^1.2.9", - "@tailwindcss/vite": "^4.3.0", - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", - "@tanstack/react-router": "^1.170.15", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", + "@tanstack/react-router": "^1.170.18", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-table": "^8.21.3", "axios": "1.18.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "form-data": "4.0.6", - "lucide-react": "^1.17.0", + "lucide-react": "^1.28.0", "next-themes": "^0.4.6", - "react": "^19.2.7", - "react-dom": "^19.2.7", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-error-boundary": "^6.1.2", - "react-hook-form": "^7.78.0", - "react-icons": "^5.6.0", + "react-hook-form": "^7.84.0", + "react-icons": "^5.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.2.1", "zod": "^4.4.3", }, "devDependencies": { - "@biomejs/biome": "^2.4.16", + "@biomejs/biome": "^2.5.6", "@hey-api/openapi-ts": "0.97.3", - "@playwright/test": "1.61.1", + "@playwright/test": "1.62.1", "@tanstack/router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", - "@types/node": "^25.9.2", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react-swc": "^4.3.1", + "@tanstack/router-plugin": "^1.168.23", + "@types/node": "^26.1.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react-swc": "^4.3.3", "dotenv": "^17.4.2", "tw-animate-css": "^1.4.0", - "typescript": "^5.9.3", - "vite": "^8.0.16", + "typescript": "^6.0.3", + "vite": "^8.2.0", }, }, "packages/react-email": { @@ -65,15 +65,15 @@ "version": "0.1.0", "dependencies": { "@react-email/components": "1.0.12", - "react": "^19.2.7", - "react-dom": "^19.2.7", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-email": "6.9.1", }, "devDependencies": { - "@biomejs/biome": "^2.4.16", + "@biomejs/biome": "^2.5.6", "@react-email/ui": "6.9.1", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", }, }, }, @@ -94,9 +94,9 @@ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], @@ -108,31 +108,27 @@ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], + "@biomejs/biome": ["@biomejs/biome@2.5.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.6", "@biomejs/cli-darwin-x64": "2.5.6", "@biomejs/cli-linux-arm64": "2.5.6", "@biomejs/cli-linux-arm64-musl": "2.5.6", "@biomejs/cli-linux-x64": "2.5.6", "@biomejs/cli-linux-x64-musl": "2.5.6", "@biomejs/cli-win32-arm64": "2.5.6", "@biomejs/cli-win32-x64": "2.5.6" }, "bin": { "biome": "bin/biome" } }, "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], @@ -206,7 +202,7 @@ "@hey-api/types": ["@hey-api/types@0.1.4", "", {}, "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg=="], - "@hookform/resolvers": ["@hookform/resolvers@5.4.0", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw=="], + "@hookform/resolvers": ["@hookform/resolvers@5.7.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "@sinclair/typebox": ">=0.25.24", "@standard-schema/spec": "^1.0.0", "@typeschema/main": ">=0.13.7", "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "arktype": "^2.0.0", "ata-validator": "^1.2.0", "class-transformer": ">=0.4.0", "class-validator": ">=0.12.0", "computed-types": "^1.0.0", "effect": "^3.10.3", "fluentvalidation-ts": "^3.0.0", "fp-ts": "^2.7.0", "io-ts": "^2.0.0", "joi": "^17.0.0", "nope-validator": ">=0.12.0", "react-hook-form": "^7.55.0", "superstruct": ">=0.12.0", "typanion": "^3.3.2", "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", "vest": ">=3.0.0", "yup": "^1.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@sinclair/typebox", "@standard-schema/spec", "@typeschema/main", "@vinejs/vine", "ajv", "ajv-errors", "ajv-formats", "arktype", "ata-validator", "class-transformer", "class-validator", "computed-types", "effect", "fluentvalidation-ts", "fp-ts", "io-ts", "joi", "nope-validator", "superstruct", "typanion", "valibot", "vest", "yup", "zod"] }, "sha512-8wS/P4UDr5sQDe4nFaV51TVyfDPrWgNIXweqG0Bs9Z5LSuzKLb+RQNPvkN2oHM5SRrJyWrVH/F+LOUcFjUyvwQ=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -272,8 +268,6 @@ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], @@ -292,89 +286,87 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], - "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + "@oxc-project/types": ["@oxc-project/types@0.143.0", "", {}, "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA=="], - "@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="], + "@playwright/test": ["@playwright/test@1.62.1", "", { "dependencies": { "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" } }, "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ=="], - "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], + "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA=="], - "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.12", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA=="], - "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.15", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="], - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-escape-keydown": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w=="], - "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.17", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.6", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.16", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g=="], - "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-callback-ref": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-callback-ref": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.0", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-rect": "1.1.4", "@radix-ui/react-use-size": "1.1.4", "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.5", "", { "dependencies": { "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], - "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.0", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ=="], + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ=="], - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.11", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.18", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA=="], - "@radix-ui/react-select": ["@radix-ui/react-select@2.3.0", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.5", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.3.7", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog=="], - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.9", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-visually-hidden": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-u6F9MmTtBSLkiXNVDrtB/yPCZarM9smNswC24YYLV/M+bth6J3Gs3vlJezEoFwKZvPvxhCpUYdUnOsNG/0XOlA=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.2", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw=="], + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw=="], - "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg=="], - "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.4", "", { "dependencies": { "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ=="], - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw=="], - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ=="], - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.5", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg=="], - - "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.3", "", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="], "@react-email/body": ["@react-email/body@0.3.0", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug=="], @@ -420,35 +412,33 @@ "@react-email/ui": ["@react-email/ui@6.9.1", "", { "dependencies": { "esbuild": "0.28.1", "next": "16.2.6" } }, "sha512-zw7KvwMu3RU/fDC9TAkegg9/qNV9foJtd3QRoetdWVFe8aQC/NyXmWTrG9IrkzVLzn5otkkxu7e7B1QelcwdiQ=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.3", "", { "os": "android", "cpu": "arm64" }, "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.3", "", { "os": "none", "cpu": "arm64" }, "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], @@ -458,75 +448,79 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@swc/core": ["@swc/core@1.15.11", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.11", "@swc/core-darwin-x64": "1.15.11", "@swc/core-linux-arm-gnueabihf": "1.15.11", "@swc/core-linux-arm64-gnu": "1.15.11", "@swc/core-linux-arm64-musl": "1.15.11", "@swc/core-linux-x64-gnu": "1.15.11", "@swc/core-linux-x64-musl": "1.15.11", "@swc/core-win32-arm64-msvc": "1.15.11", "@swc/core-win32-ia32-msvc": "1.15.11", "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w=="], + "@swc/core": ["@swc/core@1.15.47", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.27" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.47", "@swc/core-darwin-x64": "1.15.47", "@swc/core-linux-arm-gnueabihf": "1.15.47", "@swc/core-linux-arm64-gnu": "1.15.47", "@swc/core-linux-arm64-musl": "1.15.47", "@swc/core-linux-ppc64-gnu": "1.15.47", "@swc/core-linux-s390x-gnu": "1.15.47", "@swc/core-linux-x64-gnu": "1.15.47", "@swc/core-linux-x64-musl": "1.15.47", "@swc/core-win32-arm64-msvc": "1.15.47", "@swc/core-win32-ia32-msvc": "1.15.47", "@swc/core-win32-x64-msvc": "1.15.47" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg=="], - "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg=="], + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.47", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA=="], - "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA=="], + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.47", "", { "os": "darwin", "cpu": "x64" }, "sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg=="], - "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.11", "", { "os": "linux", "cpu": "arm" }, "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg=="], + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.47", "", { "os": "linux", "cpu": "arm" }, "sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g=="], - "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA=="], + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.47", "", { "os": "linux", "cpu": "arm64" }, "sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA=="], - "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w=="], + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.47", "", { "os": "linux", "cpu": "arm64" }, "sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w=="], - "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ=="], + "@swc/core-linux-ppc64-gnu": ["@swc/core-linux-ppc64-gnu@1.15.47", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ=="], - "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw=="], + "@swc/core-linux-s390x-gnu": ["@swc/core-linux-s390x-gnu@1.15.47", "", { "os": "linux", "cpu": "s390x" }, "sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ=="], - "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA=="], + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.47", "", { "os": "linux", "cpu": "x64" }, "sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ=="], - "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw=="], + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.47", "", { "os": "linux", "cpu": "x64" }, "sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ=="], - "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.11", "", { "os": "win32", "cpu": "x64" }, "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw=="], + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.47", "", { "os": "win32", "cpu": "arm64" }, "sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.47", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.47", "", { "os": "win32", "cpu": "x64" }, "sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="], + "@swc/types": ["@swc/types@0.1.28", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw=="], - "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], - "@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="], + "@tanstack/query-core": ["@tanstack/query-core@5.101.4", "", {}, "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw=="], - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.101.0", "", {}, "sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ=="], + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.101.4", "", {}, "sha512-z5IPHnDX3aUWeTWlRKLyooBQekaCAw4xRpZqPQ390RiWTDBcTynjpPT221BArw0u2+pnQMdGvPQI9YNNubBcmA=="], - "@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="], + "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="], - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.101.0", "", { "dependencies": { "@tanstack/query-devtools": "5.101.0" }, "peerDependencies": { "@tanstack/react-query": "^5.101.0", "react": "^18 || ^19" } }, "sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w=="], + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.101.4", "", { "dependencies": { "@tanstack/query-devtools": "5.101.4" }, "peerDependencies": { "@tanstack/react-query": "^5.101.4", "react": "^18 || ^19" } }, "sha512-VeK2gtmfj7kvRBjtxS7TKxt/6qKhn8VzabY4UiYMr7NV9CddjSRYRgeYyld+NpjAkgMV9dd+2Qdr8ah5I03NeA=="], - "@tanstack/react-router": ["@tanstack/react-router@1.170.15", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.13", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-GawYz7HEjj8rTUUDoT/SemDEVm63pZUO+2mOcXHY9Jl3EwMS5gFBnPu/2UvcrwRm1jN1k79fokc0d4aFmrLatg=="], + "@tanstack/react-router": ["@tanstack/react-router@1.170.18", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-wpbGYZEp/fmz1q4bn7BD8VZ+/VZ7GBqSJv5V969pU+chP8y7dquWDmKTFMohvUegb9lg12m1uPVvD6kB2wORvQ=="], "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.167.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.168.0" }, "peerDependencies": { "@tanstack/react-router": "^1.170.0", "@tanstack/router-core": "^1.170.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w=="], @@ -534,15 +528,15 @@ "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], - "@tanstack/router-core": ["@tanstack/router-core@1.171.13", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA=="], + "@tanstack/router-core": ["@tanstack/router-core@1.171.15", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-IILCDcLaItMZQ2jEmCABHY1Nhjjn5XUvwpQp3e4Nmu+vfg0BgYFuu/QASz2SwE2ZNbVMrvt8X/wxa+Gg5aErxA=="], "@tanstack/router-devtools": ["@tanstack/router-devtools@1.167.0", "", { "dependencies": { "@tanstack/react-router-devtools": "1.167.0", "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/react-router": "^1.170.0", "csstype": "^3.0.10", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["csstype"] }, "sha512-NwHy3SNoEgGraWtOstykkBPCTv7QRWBuPH49ww3prsyzQWXSSb7/2Jp7HHndpDrAIn0XNCCaxho90+6RFLSpUA=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.170.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.167.17", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.13", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-xtB9tB2Ws0tWR6Pi7nc3Qk9IYgoh1mQCKWjHqIl9tf6BNUpKoqniJoPAQ4+LGrK8FeZYU0o0p/qlZEyj9FAulA=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.167.21", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.15", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-m3oXZyienj8owialdyoZ0txHQrnEx/Ra+D9kWtar5fC2cWZr5Pvxl86VY2mX5RRLC5QLKLeRGT1x4HV95wHVDQ=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.18", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.13", "@tanstack/router-generator": "1.167.17", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.15", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-MofS28/axfnfnhOD2RSgJEaU882aX5RsAzhGz5Vc4XhAmvCjy919u9JrNs4QsTWFbTD1P7IJ8WFlFVsrg0pStg=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.23", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.15", "@tanstack/router-generator": "1.167.21", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.18", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-0+PIcvnaAimFwjoEIeV3h7LKjzC8zNnp7pH2UamdKwQ9QlY99WU9V0Xl0zbM0i9hrUa/mKgWPDAzELmPUu5fMA=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], @@ -552,21 +546,19 @@ "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="], - "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.3.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w=="], + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.3.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1", "@swc/core": "^1.15.46" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-bti8ZAcvz4Lh6/e4Uk2k3aa1TiUXbbMsahuqOHvd3MveFTkKDZOA6wQVkpj7J/+tepX/wGfe+lsGh/t24HTXMQ=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -692,7 +684,7 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.24.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ=="], + "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -794,35 +786,35 @@ "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], + "lucide-react": ["lucide-react@1.28.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -846,7 +838,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -880,17 +872,17 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "picospinner": ["picospinner@3.1.2", "", {}, "sha512-0Z++uU8mvB0EvCPAEUq9BGiPcSravzJ+RPdXfjYlzXffqsogisiKRQqI6ctrSSJ6n985vxrgKtQ5n4sRINcJZg=="], "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], - "playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="], + "playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="], - "playwright-core": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="], + "playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], @@ -904,17 +896,17 @@ "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], - "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], - "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], "react-email": ["react-email@6.9.1", "", { "dependencies": { "@babel/parser": "7.29.2", "@babel/traverse": "7.29.0", "@react-email/render": ">=2.1.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "conf": "^15.0.2", "css-tree": "3.2.1", "debounce": "^2.0.0", "esbuild": "^0.28.0", "glob": "^13.0.6", "jiti": "2.6.1", "log-symbols": "^7.0.0", "marked": "^15.0.12", "mime-types": "^3.0.0", "normalize-path": "^3.0.0", "nypm": "0.6.6", "picospinner": "^3.0.0", "prismjs": "^1.30.0", "prompts": "2.4.2", "socket.io": "^4.8.1", "tailwindcss": "^4.1.18", "tsconfig-paths": "4.2.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" }, "bin": { "email": "./dist/cli/index.mjs" } }, "sha512-uUDRgFukMUXRlrsCNGlA0PZuUlQ44faI9hT/D7uMjozdumLBdHjfQttQswRYLjyLW8fFtg2HBrxAESbFU4ZKKA=="], "react-error-boundary": ["react-error-boundary@6.1.2", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng=="], - "react-hook-form": ["react-hook-form@7.78.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA=="], + "react-hook-form": ["react-hook-form@7.84.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-+hWvQP6GLco56mDwrbU4XnHix8t1z90ltZsDIrREl+jnQFQxYLX8oAzqe/Xn8nHpmoXTY5M6oEXrAhbP1qevNQ=="], - "react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="], + "react-icons": ["react-icons@5.7.0", "", { "peerDependencies": { "react": "*" } }, "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw=="], "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], @@ -928,7 +920,7 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], + "rolldown": ["rolldown@1.2.3", "", { "dependencies": { "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.3", "@rolldown/binding-darwin-arm64": "1.2.3", "@rolldown/binding-darwin-x64": "1.2.3", "@rolldown/binding-freebsd-x64": "1.2.3", "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", "@rolldown/binding-linux-arm64-gnu": "1.2.3", "@rolldown/binding-linux-arm64-musl": "1.2.3", "@rolldown/binding-linux-ppc64-gnu": "1.2.3", "@rolldown/binding-linux-s390x-gnu": "1.2.3", "@rolldown/binding-linux-x64-gnu": "1.2.3", "@rolldown/binding-linux-x64-musl": "1.2.3", "@rolldown/binding-openharmony-arm64": "1.2.3", "@rolldown/binding-win32-arm64-msvc": "1.2.3", "@rolldown/binding-win32-x64-msvc": "1.2.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], @@ -972,7 +964,7 @@ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], @@ -984,17 +976,15 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], @@ -1008,7 +998,7 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], + "vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], @@ -1026,8 +1016,6 @@ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/core/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/core/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], @@ -1036,31 +1024,39 @@ "@babel/core/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/core/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/generator/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], - "@babel/generator/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], - "@babel/parser/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + "@babel/helpers/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], "@babel/template/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - "@babel/traverse/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + "@babel/template/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], "@hey-api/openapi-ts/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@react-email/tailwind/tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + + "@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], @@ -1082,18 +1078,28 @@ "@tanstack/router-utils/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@tanstack/router-utils/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "@types/cors/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + + "@types/ws/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], "babel-dead-code-elimination/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "babel-dead-code-elimination/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.16", "", { "bin": "dist/cli.js" }, "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw=="], "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "engine.io/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "es-set-tostringtag/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -1106,15 +1112,17 @@ "react-email/@react-email/render": ["@react-email/render@2.1.0", "", { "dependencies": { "entities": "^4.5.0", "html-to-text": "^9.0.5", "html5parser": "^3.0.0", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F+zE3O6d6sW6Aj2UjvZAA17R7tJKM7kcq2mgV6k4HCT8jeLLFaVP2txMtH1lgqYFRMZ0Gxsd37q2PRyiXLXXxA=="], - "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": "bin/esbuild" }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "react-email/tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - "tsx/get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "unplugin/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], @@ -1122,82 +1130,82 @@ "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - "@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], - "@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@babel/helpers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - "@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@babel/helpers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "@tanstack/router-utils/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@tanstack/router-utils/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@tanstack/router-utils/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "@types/cors/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "@types/ws/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "babel-dead-code-elimination/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "babel-dead-code-elimination/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "babel-dead-code-elimination/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "babel-dead-code-elimination/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "engine.io/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "next/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - - "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - - "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - - "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - - "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - - "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - - "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - - "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], - - "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], - - "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], - - "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], - - "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], - - "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], - - "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], - - "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], - - "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], - - "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], - - "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], - - "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], - - "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], - - "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], - - "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], - - "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "babel-dead-code-elimination/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], } } diff --git a/frontend/Dockerfile.playwright b/frontend/Dockerfile.playwright index 6fedc8d0..63d90283 100644 --- a/frontend/Dockerfile.playwright +++ b/frontend/Dockerfile.playwright @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.61.1-noble +FROM mcr.microsoft.com/playwright:v1.62.1-noble WORKDIR /app diff --git a/frontend/biome.json b/frontend/biome.json index d24f85d2..ccee26c9 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "files": { "includes": [ @@ -16,7 +16,7 @@ "linter": { "enabled": true, "rules": { - "recommended": true, + "preset": "recommended", "suspicious": { "noExplicitAny": "off", "noArrayIndexKey": "off" @@ -42,5 +42,17 @@ "parser": { "tailwindDirectives": true } - } + }, + "overrides": [ + { + "includes": ["**/*.svg"], + "linter": { + "rules": { + "a11y": { + "noSvgWithoutTitle": "off" + } + } + } + } + ] } diff --git a/frontend/package.json b/frontend/package.json index a59f4593..9a67018b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,54 +13,54 @@ "test:ui": "bunx playwright test --ui" }, "dependencies": { - "@hookform/resolvers": "^5.4.0", - "@radix-ui/react-avatar": "^1.1.12", - "@radix-ui/react-checkbox": "^1.3.4", - "@radix-ui/react-dialog": "^1.1.16", - "@radix-ui/react-dropdown-menu": "^2.1.17", - "@radix-ui/react-label": "^2.1.9", - "@radix-ui/react-radio-group": "^1.4.0", - "@radix-ui/react-scroll-area": "^1.2.11", - "@radix-ui/react-select": "^2.3.0", - "@radix-ui/react-separator": "^1.1.9", + "@hookform/resolvers": "^5.7.1", + "@radix-ui/react-avatar": "^1.2.6", + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-radio-group": "^1.4.7", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "^1.1.14", - "@radix-ui/react-tooltip": "^1.2.9", - "@tailwindcss/vite": "^4.3.0", - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", - "@tanstack/react-router": "^1.170.15", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", + "@tanstack/react-router": "^1.170.18", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-table": "^8.21.3", "axios": "1.18.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "form-data": "4.0.6", - "lucide-react": "^1.17.0", + "lucide-react": "^1.28.0", "next-themes": "^0.4.6", - "react": "^19.2.7", - "react-dom": "^19.2.7", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-error-boundary": "^6.1.2", - "react-hook-form": "^7.78.0", - "react-icons": "^5.6.0", + "react-hook-form": "^7.84.0", + "react-icons": "^5.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.2.1", "zod": "^4.4.3" }, "devDependencies": { - "@biomejs/biome": "^2.4.16", + "@biomejs/biome": "^2.5.6", "@hey-api/openapi-ts": "0.97.3", - "@playwright/test": "1.61.1", + "@playwright/test": "1.62.1", "@tanstack/router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", - "@types/node": "^25.9.2", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react-swc": "^4.3.1", + "@tanstack/router-plugin": "^1.168.23", + "@types/node": "^26.1.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react-swc": "^4.3.3", "dotenv": "^17.4.2", "tw-animate-css": "^1.4.0", - "typescript": "^5.9.3", - "vite": "^8.0.16" + "typescript": "^6.0.3", + "vite": "^8.2.0" } } diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 08d665fe..b45f83a1 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -9,29 +9,18 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as SignupRouteImport } from './routes/signup' -import { Route as ResetPasswordRouteImport } from './routes/reset-password' -import { Route as RecoverPasswordRouteImport } from './routes/recover-password' -import { Route as LoginRouteImport } from './routes/login' import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as LoginRouteImport } from './routes/login' +import { Route as RecoverPasswordRouteImport } from './routes/recover-password' +import { Route as ResetPasswordRouteImport } from './routes/reset-password' +import { Route as SignupRouteImport } from './routes/signup' import { Route as LayoutIndexRouteImport } from './routes/_layout/index' -import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings' -import { Route as LayoutItemsRouteImport } from './routes/_layout/items' import { Route as LayoutAdminRouteImport } from './routes/_layout/admin' +import { Route as LayoutItemsRouteImport } from './routes/_layout/items' +import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings' -const SignupRoute = SignupRouteImport.update({ - id: '/signup', - path: '/signup', - getParentRoute: () => rootRouteImport, -} as any) -const ResetPasswordRoute = ResetPasswordRouteImport.update({ - id: '/reset-password', - path: '/reset-password', - getParentRoute: () => rootRouteImport, -} as any) -const RecoverPasswordRoute = RecoverPasswordRouteImport.update({ - id: '/recover-password', - path: '/recover-password', +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', getParentRoute: () => rootRouteImport, } as any) const LoginRoute = LoginRouteImport.update({ @@ -39,8 +28,19 @@ const LoginRoute = LoginRouteImport.update({ path: '/login', getParentRoute: () => rootRouteImport, } as any) -const LayoutRoute = LayoutRouteImport.update({ - id: '/_layout', +const RecoverPasswordRoute = RecoverPasswordRouteImport.update({ + id: '/recover-password', + path: '/recover-password', + getParentRoute: () => rootRouteImport, +} as any) +const ResetPasswordRoute = ResetPasswordRouteImport.update({ + id: '/reset-password', + path: '/reset-password', + getParentRoute: () => rootRouteImport, +} as any) +const SignupRoute = SignupRouteImport.update({ + id: '/signup', + path: '/signup', getParentRoute: () => rootRouteImport, } as any) const LayoutIndexRoute = LayoutIndexRouteImport.update({ @@ -48,9 +48,9 @@ const LayoutIndexRoute = LayoutIndexRouteImport.update({ path: '/', getParentRoute: () => LayoutRoute, } as any) -const LayoutSettingsRoute = LayoutSettingsRouteImport.update({ - id: '/settings', - path: '/settings', +const LayoutAdminRoute = LayoutAdminRouteImport.update({ + id: '/admin', + path: '/admin', getParentRoute: () => LayoutRoute, } as any) const LayoutItemsRoute = LayoutItemsRouteImport.update({ @@ -58,9 +58,9 @@ const LayoutItemsRoute = LayoutItemsRouteImport.update({ path: '/items', getParentRoute: () => LayoutRoute, } as any) -const LayoutAdminRoute = LayoutAdminRouteImport.update({ - id: '/admin', - path: '/admin', +const LayoutSettingsRoute = LayoutSettingsRouteImport.update({ + id: '/settings', + path: '/settings', getParentRoute: () => LayoutRoute, } as any) @@ -140,25 +140,11 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/signup': { - id: '/signup' - path: '/signup' - fullPath: '/signup' - preLoaderRoute: typeof SignupRouteImport - parentRoute: typeof rootRouteImport - } - '/reset-password': { - id: '/reset-password' - path: '/reset-password' - fullPath: '/reset-password' - preLoaderRoute: typeof ResetPasswordRouteImport - parentRoute: typeof rootRouteImport - } - '/recover-password': { - id: '/recover-password' - path: '/recover-password' - fullPath: '/recover-password' - preLoaderRoute: typeof RecoverPasswordRouteImport + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport parentRoute: typeof rootRouteImport } '/login': { @@ -168,11 +154,25 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginRouteImport parentRoute: typeof rootRouteImport } - '/_layout': { - id: '/_layout' - path: '' - fullPath: '/' - preLoaderRoute: typeof LayoutRouteImport + '/recover-password': { + id: '/recover-password' + path: '/recover-password' + fullPath: '/recover-password' + preLoaderRoute: typeof RecoverPasswordRouteImport + parentRoute: typeof rootRouteImport + } + '/reset-password': { + id: '/reset-password' + path: '/reset-password' + fullPath: '/reset-password' + preLoaderRoute: typeof ResetPasswordRouteImport + parentRoute: typeof rootRouteImport + } + '/signup': { + id: '/signup' + path: '/signup' + fullPath: '/signup' + preLoaderRoute: typeof SignupRouteImport parentRoute: typeof rootRouteImport } '/_layout/': { @@ -182,11 +182,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutIndexRouteImport parentRoute: typeof LayoutRoute } - '/_layout/settings': { - id: '/_layout/settings' - path: '/settings' - fullPath: '/settings' - preLoaderRoute: typeof LayoutSettingsRouteImport + '/_layout/admin': { + id: '/_layout/admin' + path: '/admin' + fullPath: '/admin' + preLoaderRoute: typeof LayoutAdminRouteImport parentRoute: typeof LayoutRoute } '/_layout/items': { @@ -196,11 +196,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutItemsRouteImport parentRoute: typeof LayoutRoute } - '/_layout/admin': { - id: '/_layout/admin' - path: '/admin' - fullPath: '/admin' - preLoaderRoute: typeof LayoutAdminRouteImport + '/_layout/settings': { + id: '/_layout/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof LayoutSettingsRouteImport parentRoute: typeof LayoutRoute } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 00d9148b..6bc18dc6 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -17,7 +17,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "baseUrl": ".", + "types": ["node"], "paths": { "@/*": ["./src/*"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index aff347ea..92bebb94 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ }, resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + "@": path.resolve(import.meta.dirname, "./src"), }, }, plugins: [ diff --git a/packages/react-email/package.json b/packages/react-email/package.json index 8b12cfbf..be6e2760 100644 --- a/packages/react-email/package.json +++ b/packages/react-email/package.json @@ -10,14 +10,14 @@ }, "dependencies": { "@react-email/components": "1.0.12", - "react": "^19.2.7", - "react-dom": "^19.2.7", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-email": "6.9.1" }, "devDependencies": { - "@biomejs/biome": "^2.4.16", + "@biomejs/biome": "^2.5.6", "@react-email/ui": "6.9.1", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3" + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4" } } From b55b3122ab446e666d9dc4fc933c3c17aff3a137 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:08:26 +0000 Subject: [PATCH 65/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 9755243c..3b7ada2a 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,10 @@ * ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91). +### Internal + +* ⬆ Bump the npm-packages group across 1 directory with 33 updates. PR [#2433](https://github.com/fastapi/full-stack-fastapi-template/pull/2433) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.11.0 (2026-08-11) ### Features From 6b9fa4113c6ce32b143b8c850785c5431af00848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 11 Aug 2026 21:35:45 +0200 Subject: [PATCH 66/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20Docker=20?= =?UTF-8?q?Compose=20deployment=20(#2435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 37 ------------ .../{deploy-staging.yml => deploy.yml} | 9 ++- deployment.md | 60 ++++--------------- development.md | 2 +- 4 files changed, 17 insertions(+), 91 deletions(-) delete mode 100644 .github/workflows/deploy-production.yml rename .github/workflows/{deploy-staging.yml => deploy.yml} (91%) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml deleted file mode 100644 index e62fe953..00000000 --- a/.github/workflows/deploy-production.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Deploy to Production - -on: - release: - types: - - published - -permissions: - contents: read - -jobs: - deploy: - environment: production - # Do not deploy in the main repository, only in user projects - if: github.repository_owner != 'fastapi' - runs-on: - - self-hosted - - production - env: - DOMAIN: ${{ secrets.DOMAIN }} - SECRET_KEY: ${{ secrets.SECRET_KEY }} - FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} - FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} - SMTP_HOST: ${{ secrets.SMTP_HOST }} - SMTP_USER: ${{ secrets.SMTP_USER }} - SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} - EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - run: docker compose -f compose.yml -f compose.deploy.yml build - - run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh - - run: docker compose -f compose.yml -f compose.deploy.yml up -d diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy.yml similarity index 91% rename from .github/workflows/deploy-staging.yml rename to .github/workflows/deploy.yml index 1c35943d..e1f6640c 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy.yml @@ -1,21 +1,20 @@ -name: Deploy to Staging +name: Deploy on: push: branches: - master + paths-ignore: + - release-notes.md permissions: contents: read jobs: deploy: - environment: staging # Do not deploy in the main repository, only in user projects if: github.repository_owner != 'fastapi' - runs-on: - - self-hosted - - staging + runs-on: self-hosted env: DOMAIN: ${{ secrets.DOMAIN }} SECRET_KEY: ${{ secrets.SECRET_KEY }} diff --git a/deployment.md b/deployment.md index 331ccbac..c1e5adce 100644 --- a/deployment.md +++ b/deployment.md @@ -4,13 +4,13 @@ You can deploy the project using Docker Compose to a remote server. The deployment Docker Compose configuration includes Traefik to handle HTTPS and route incoming traffic to the application. -You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions. +The included GitHub Actions workflow can deploy the application automatically. But you have to configure a couple things first. 🤓 ## Preparation -* Have a remote server ready and available. Use a separate server for each environment, for example one for staging and one for production. +* Have a remote server ready and available. * Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com` and `adminer.fastapi-project.example.com`. * Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop). @@ -82,12 +82,15 @@ You can set several other environment variables: * `EMAILS_FROM_EMAIL`: The email account to send emails from. * `SENTRY_DSN`: The DSN for Sentry, if you are using it. -## GitHub Actions Environment Variables +## GitHub Repository Automation -There are some environment variables only used by GitHub Actions that you can configure: +Install the following GitHub Apps to enable the included repository automation: -* `LATEST_CHANGES`: Used by the GitHub Action [latest-changes](https://github.com/tiangolo/latest-changes) to automatically add release notes based on the PRs merged. It's a personal access token, read the docs for details. -* `SMOKESHOW_AUTH_KEY`: Used to handle and publish the code coverage using [Smokeshow](https://github.com/samuelcolvin/smokeshow), follow their instructions to create a (free) Smokeshow key. +* [Latest Changes](https://github.com/apps/latest-changes) updates `release-notes.md` when a pull request is merged. +* [PR Push](https://github.com/apps/pr-push) lets the pre-commit workflow push automated fixes to pull request branches. +* [PR Submit](https://github.com/apps/pr-submit) lets the **Bump pre-commit hooks** and **Prepare Release** workflows create pull requests. + +To publish code coverage with [Smokeshow](https://github.com/samuelcolvin/smokeshow), add `SMOKESHOW_AUTH_KEY` as a repository secret. ### Deploy with Docker Compose @@ -106,7 +109,7 @@ The `compose.deploy.yml` file adds the deployment settings to the shared configu You can use GitHub Actions to deploy your project automatically. 😎 -There are already two environment deployments configured, `staging` and `production`. Each environment should be deployed to a separate server. 🚀 +The included `deploy.yml` workflow deploys the application whenever changes are pushed to `master`, including when a pull request is merged. 🚀 ### Install GitHub Actions Runner @@ -136,8 +139,6 @@ cd * [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository). -* When asked about labels, add a label for the environment, e.g. `production`. You can also add labels later. - After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost. To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user: @@ -180,17 +181,9 @@ cd /home/github/actions-runner You can read more about it in the official guide: [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service). -### Configure GitHub Environments +### Set Repository Secrets -The deployment workflows use [GitHub Environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments) for `staging` and `production`. This enables environment-specific secrets, deployment protection rules (e.g. required reviewers, wait timers), and deployment status tracking. - -To configure them, go to your repository's **Settings** > **Environments** and create the `staging` and `production` environments. - -### Set Secrets - -For each GitHub Environment (`staging` and `production`), configure the required secrets as [environment secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-an-environment). Environment secrets are preferred over [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository) because they are scoped to the specific environment, reducing exposure and aligning with any protection rules you configure. - -The deployment workflows require these secrets: +In your repository, go to **Settings** > **Secrets and variables** > **Actions** and add the following [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository): * `DOMAIN` * `FIRST_SUPERUSER` @@ -207,41 +200,12 @@ To enable emails, configure these additional secrets with the values from your e To enable Sentry, configure the `SENTRY_DSN` secret. -## GitHub Action Deployment Workflows - -There are GitHub Action workflows in the `.github/workflows` directory already configured for deploying to the environments (GitHub Actions runners with the labels): - -* `staging`: after pushing (or merging) to the branch `master`. -* `production`: after publishing a release. - -Both workflows are associated with their respective GitHub Environments, so deployments will be visible in the repository's **Environments** section and will respect any protection rules you configure. - -### Prepare a Release - -Install the [PR Submit GitHub App](https://github.com/apps/pr-submit) in your repository to enable the **Prepare Release** workflow. - -Run the workflow manually from the **Actions** tab and select the version bump. It creates a pull request that updates the release notes. When you merge that pull request, the **Create Draft Release** workflow creates a draft GitHub release with the corresponding release notes. - -Review and publish the draft release to trigger the production deployment. - -If you need to add extra environments you could use those as a starting point. - ## URLs Replace `fastapi-project.example.com` with your domain. -### Production - Application (frontend and API): `https://fastapi-project.example.com` Interactive API docs: `https://fastapi-project.example.com/docs` Adminer: `https://adminer.fastapi-project.example.com` - -### Staging - -Application (frontend and API): `https://staging.fastapi-project.example.com` - -Interactive API docs: `https://staging.fastapi-project.example.com/docs` - -Adminer: `https://adminer.staging.fastapi-project.example.com` diff --git a/development.md b/development.md index 198a5ca3..07eb0491 100644 --- a/development.md +++ b/development.md @@ -156,7 +156,7 @@ biome check..............................................................Passed ## URLs -The production or staging URLs would use these same paths, but with your own domain. +The deployed URLs use these same paths, but with your own domain. ### Development URLs From eae2d6701d67084b677fb4030832a2e9babe0c10 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:35:48 +0000 Subject: [PATCH 67/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 3b7ada2a..7de3940d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ ### Refactors +* ♻️ Simplify Docker Compose deployment. PR [#2435](https://github.com/fastapi/full-stack-fastapi-template/pull/2435) by [@tiangolo](https://github.com/tiangolo). * ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91). ### Internal From ddb3c34f1956c028bcb10985ce60c3c147c52d8c Mon Sep 17 00:00:00 2001 From: "pr-submit[bot]" <314742883+pr-submit[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:57:14 +0200 Subject: [PATCH 68/91] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11.1?= =?UTF-8?q?=20(#2437)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pr-submit[bot] --- release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-notes.md b/release-notes.md index 7de3940d..b7298389 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.11.1 (2026-08-11) + ### Refactors * ♻️ Simplify Docker Compose deployment. PR [#2435](https://github.com/fastapi/full-stack-fastapi-template/pull/2435) by [@tiangolo](https://github.com/tiangolo). From 2d8fb3e4defa61d84ac8bd23faa93b626cda7f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 11 Aug 2026 23:16:35 +0200 Subject: [PATCH 69/91] =?UTF-8?q?=E2=98=81=EF=B8=8F=20Add=20FastAPI=20Clou?= =?UTF-8?q?d=20deployment=20(#2438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .fastapicloudignore | 2 + .github/workflows/deploy-docker-compose.yml | 41 ++++ .github/workflows/deploy.yml | 52 +++-- README.md | 25 +-- deployment-docker-compose.md | 129 ++++++++++++ deployment.md | 216 +++++--------------- frontend/README.md | 4 + 7 files changed, 271 insertions(+), 198 deletions(-) create mode 100644 .fastapicloudignore create mode 100644 .github/workflows/deploy-docker-compose.yml create mode 100644 deployment-docker-compose.md diff --git a/.fastapicloudignore b/.fastapicloudignore new file mode 100644 index 00000000..de3842d1 --- /dev/null +++ b/.fastapicloudignore @@ -0,0 +1,2 @@ +.env +!backend/app/frontend/ diff --git a/.github/workflows/deploy-docker-compose.yml b/.github/workflows/deploy-docker-compose.yml new file mode 100644 index 00000000..4062687b --- /dev/null +++ b/.github/workflows/deploy-docker-compose.yml @@ -0,0 +1,41 @@ +name: Deploy with Docker Compose + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: deploy-docker-compose + cancel-in-progress: false + +jobs: + deploy: + # Do not deploy the template repository, only projects created from it + if: github.repository != 'fastapi/full-stack-fastapi-template' + runs-on: self-hosted + timeout-minutes: 30 + env: + DOMAIN: ${{ vars.DOMAIN }} + PROJECT_NAME: ${{ vars.PROJECT_NAME }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + FIRST_SUPERUSER: ${{ vars.FIRST_SUPERUSER }} + FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} + SMTP_HOST: ${{ vars.SMTP_HOST }} + SMTP_USER: ${{ vars.SMTP_USER }} + SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} + EMAILS_FROM_EMAIL: ${{ vars.EMAILS_FROM_EMAIL }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + SENTRY_DSN: ${{ vars.SENTRY_DSN }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Build + run: docker compose -f compose.yml -f compose.deploy.yml build + - name: Prepare database + run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh + - name: Start application + run: docker compose -f compose.yml -f compose.deploy.yml up -d diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e1f6640c..82e7efd2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,31 +6,49 @@ on: - master paths-ignore: - release-notes.md + workflow_dispatch: permissions: contents: read jobs: deploy: - # Do not deploy in the main repository, only in user projects - if: github.repository_owner != 'fastapi' - runs-on: self-hosted - env: - DOMAIN: ${{ secrets.DOMAIN }} - SECRET_KEY: ${{ secrets.SECRET_KEY }} - FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} - FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} - SMTP_HOST: ${{ secrets.SMTP_HOST }} - SMTP_USER: ${{ secrets.SMTP_USER }} - SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} - EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + # Do not deploy the template repository, only projects created from it + if: github.repository != 'fastapi/full-stack-fastapi-template' + runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + fetch-depth: 0 persist-credentials: false - - run: docker compose -f compose.yml -f compose.deploy.yml build - - run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh - - run: docker compose -f compose.yml -f compose.deploy.yml up -d + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.12 + - name: Install frontend dependencies + run: bun ci + - name: Build frontend + run: bun run --filter frontend build + env: + # Override frontend/.env so the deployed frontend uses the same origin as the API + VITE_API_URL: "" + - name: Set up uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.18" + - name: Prepare database + run: uv run bash scripts/prestart.sh + working-directory: backend + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + PROJECT_NAME: ${{ vars.PROJECT_NAME }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + FIRST_SUPERUSER: ${{ vars.FIRST_SUPERUSER }} + FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} + - name: Deploy to FastAPI Cloud + run: uv run fastapi deploy + env: + FASTAPI_CLOUD_TOKEN: ${{ secrets.FASTAPI_CLOUD_TOKEN }} + FASTAPI_CLOUD_APP_ID: ${{ secrets.FASTAPI_CLOUD_APP_ID }} diff --git a/README.md b/README.md index 010e8fc9..f12724cf 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,21 @@ - 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management. - 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database. - 🚀 [React](https://react.dev) for the frontend. - - 🧩 Built into the backend image and served by FastAPI on the same domain as the API. + - 🧩 Built into the backend application and served by FastAPI on the same domain as the API. - 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack. - 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components. - 🤖 An automatically generated frontend client. - 🧪 [Playwright](https://playwright.dev) for End-to-End testing. - 🦇 Dark mode support. -- 🐋 [Docker Compose](https://www.docker.com) for local services and deployment. +- ☁️ [FastAPI Cloud](https://fastapicloud.com) for deployment. +- 🐋 [Docker Compose](https://www.docker.com) for local services and self-hosted deployment. + - 📞 [Traefik](https://traefik.io) as a reverse proxy with automatic HTTPS. - 🔒 Secure password hashing by default. - 🔑 JWT (JSON Web Token) authentication. - 📫 Email based password recovery. - 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development. - ✅ Tests with [Pytest](https://pytest.org). -- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer. -- 🚢 Deployment instructions using Docker Compose with automatic HTTPS provided by Traefik. +- 🚢 Deployment with FastAPI Cloud or self-hosting with Docker Compose and Traefik. - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. ### Dashboard Login @@ -123,17 +124,7 @@ git merge --continue ### Configure -You can then update configs in the `.env` files to customize your configurations. - -Before deploying it, make sure you change at least the values for: - -- `SECRET_KEY` -- `FIRST_SUPERUSER_PASSWORD` -- `POSTGRES_PASSWORD` - -You can (and should) pass these as environment variables from secrets. - -Read the [deployment.md](./deployment.md) docs for more details. +You can update the local settings in the `.env` file. Before deploying, configure the environment variables and secrets described in the [FastAPI Cloud deployment guide](./deployment.md) or the [Docker Compose deployment guide](./deployment-docker-compose.md). Never deploy with the default `changethis` values. ### Generate Secret Keys @@ -159,11 +150,13 @@ Frontend docs: [frontend/README.md](./frontend/README.md). Deployment docs: [deployment.md](./deployment.md). +Docker Compose deployment: [deployment-docker-compose.md](./deployment-docker-compose.md). + ## Development General development docs: [development.md](./development.md). -This includes the local FastAPI and Vite workflow, Docker Compose services, custom local domains, `.env` configuration, and more. +This includes the local FastAPI and Vite workflow, Docker Compose services, `.env` configuration, and more. ## Release Notes diff --git a/deployment-docker-compose.md b/deployment-docker-compose.md new file mode 100644 index 00000000..5c43fd54 --- /dev/null +++ b/deployment-docker-compose.md @@ -0,0 +1,129 @@ +# FastAPI Project - Docker Compose Deployment + +You can deploy the project to your own remote server with Docker Compose. The deployment configuration includes Traefik to handle HTTPS and route incoming traffic to the application. + +## Preparation + +* Have a remote server ready and available. +* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, such as `fastapi-project.example.com` and `adminer.fastapi-project.example.com`. +* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop). + +## Copy the Code + +```bash +rsync -av --exclude=".git/" --filter=":- .gitignore" ./ root@your-server.example.com:/root/code/app/ +``` + +The `--filter=":- .gitignore"` option tells `rsync` to use the same ignore rules as Git, excluding files such as the Python virtual environment. + +## Configure the Application + +### Environment Variables + +Set the application domain, project name, and first superuser email: + +```bash +export DOMAIN=fastapi-project.example.com +export PROJECT_NAME="Full Stack FastAPI Project" +export FIRST_SUPERUSER=admin@example.com +``` + +You can also configure these environment variables as needed: + +* `SMTP_HOST`: The SMTP server host from your email provider. +* `SMTP_USER`: The SMTP server user. +* `EMAILS_FROM_EMAIL`: The email account used to send emails. +* `SENTRY_DSN`: The DSN for Sentry. + +### Secrets + +Generate and set secure values for the database password, token signing key, and first superuser password: + +```bash +export POSTGRES_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" +export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" +export FIRST_SUPERUSER_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" +``` + +To use an authenticated email provider, also set `SMTP_PASSWORD`. + +## Deploy + +```bash +cd /root/code/app/ +docker compose -f compose.yml -f compose.deploy.yml build +docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh +docker compose -f compose.yml -f compose.deploy.yml up -d +``` + +The `compose.deploy.yml` file adds HTTPS and automatic certificate handling to the shared `compose.yml` configuration. Explicitly listing both files excludes the local settings from `compose.override.yml`. + +The backend Docker image builds the frontend, so the server does not need Bun or prebuilt frontend files. + +## Deploy with GitHub Actions + +The included `.github/workflows/deploy-docker-compose.yml` workflow runs the deployment commands on the server when manually triggered from GitHub Actions. + +Use a self-hosted runner only for a repository whose contributors and workflow code you trust. GitHub recommends using self-hosted runners with private repositories because workflows execute directly on the runner machine. + +### Configure Repository Variables and Secrets + +In the repository, go to **Settings** > **Secrets and variables** > **Actions** and add these repository variables: + +* `DOMAIN` +* `PROJECT_NAME` +* `FIRST_SUPERUSER` + +To enable emails, add these optional repository variables: + +* `SMTP_HOST` +* `SMTP_USER` +* `EMAILS_FROM_EMAIL` + +To enable Sentry, add the optional `SENTRY_DSN` repository variable. + +Add these repository secrets: + +* `POSTGRES_PASSWORD` +* `SECRET_KEY` +* `FIRST_SUPERUSER_PASSWORD` + +To use an authenticated email provider, add the optional `SMTP_PASSWORD` repository secret. + +### Install a Self-Hosted Runner + +On the server, create a dedicated user and grant it access to Docker: + +```bash +sudo adduser github +sudo usermod -aG docker github +sudo su - github +``` + +In the GitHub repository, go to **Settings** > **Actions** > **Runners**, select **New self-hosted runner**, choose Linux, and follow the commands GitHub provides to download, configure, and register the runner. Install it in `/home/github/actions-runner`. + +After registering the runner, exit the `github` user session and install the runner as a system service: + +```bash +exit +cd /home/github/actions-runner +sudo ./svc.sh install github +sudo ./svc.sh start +sudo ./svc.sh status +``` + +See GitHub's guides for [adding a self-hosted runner](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners) and [configuring the runner as a service](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/configure-the-application?platform=linux). + +### Run the Deployment + +When the runner is online, open the repository's **Actions** tab, select **Deploy with Docker Compose**, and select **Run workflow**. + +## URLs + +Replace `fastapi-project.example.com` with your domain. + +Application (frontend and API): `https://fastapi-project.example.com` + +Interactive API docs: `https://fastapi-project.example.com/docs` + +Adminer: `https://adminer.fastapi-project.example.com` diff --git a/deployment.md b/deployment.md index c1e5adce..8e445ce5 100644 --- a/deployment.md +++ b/deployment.md @@ -1,86 +1,90 @@ # FastAPI Project - Deployment -You can deploy the project using Docker Compose to a remote server. +Deploy the project to [FastAPI Cloud](https://fastapicloud.com) with the included GitHub Actions workflow. -The deployment Docker Compose configuration includes Traefik to handle HTTPS and route incoming traffic to the application. +## Create the FastAPI Cloud Application -The included GitHub Actions workflow can deploy the application automatically. +Create an application in FastAPI Cloud and set its [Application Directory](https://fastapicloud.com/docs/builds-and-deployments/application-directory/) to `backend`. -But you have to configure a couple things first. 🤓 +Connect a PostgreSQL database using the [Neon](https://fastapicloud.com/docs/integrations/neon-integration/) or [Supabase](https://fastapicloud.com/docs/integrations/supabase-integration/) integration. Both integrations configure a `DATABASE_URL` secret automatically. You can also configure `DATABASE_URL` manually for another PostgreSQL provider. -## Preparation +## Configure the Application -* Have a remote server ready and available. -* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com` and `adminer.fastapi-project.example.com`. -* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop). +### Environment Variables -## Deploy the FastAPI Project +Add these required [environment variables](https://fastapicloud.com/docs/builds-and-deployments/environment-variables/) to the FastAPI Cloud application: -You can deploy your FastAPI project with Docker Compose. +* `PROJECT_NAME`: The name of the project, used in the API documentation and emails. +* `FIRST_SUPERUSER`: The email address of the first superuser. +* `FRONTEND_HOST`: The public URL of the application, such as the generated `https://your-app.fastapicloud.dev` URL or a custom domain. -**Note**: You might want to jump ahead to the section about Continuous Deployment with GitHub Actions. +To enable emails, add these optional environment variables with values from your email provider: -## Copy the Code +* `SMTP_HOST` +* `SMTP_USER` +* `EMAILS_FROM_EMAIL` -```bash -rsync -av --exclude=".git/" --filter=":- .gitignore" ./ root@your-server.example.com:/root/code/app/ -``` +To enable Sentry, configure `SENTRY_DSN`. -Note: `--filter=":- .gitignore"` tells `rsync` to use the same rules as git, ignore files ignored by git, like the Python virtual environment. +### Secrets -## Environment Variables +Add these required values and mark them as secrets: -You need to set some environment variables first. +* `SECRET_KEY`: A secret key used to sign security tokens. +* `FIRST_SUPERUSER_PASSWORD`: The password of the first superuser. +* `DATABASE_URL`: The PostgreSQL connection URL, configured automatically when using a database integration. -### Generate secret keys +To enable emails with an authenticated provider, add `SMTP_PASSWORD` as a secret. -Some environment variables in the `.env` file have a default value of `changethis`. - -You have to change them with a secret key, to generate secret keys you can run the following command: +You can generate secure values for `SECRET_KEY` and `FIRST_SUPERUSER_PASSWORD` with: ```bash python -c "import secrets; print(secrets.token_urlsafe(32))" ``` -Copy the content and use that as password / secret key. And run that again to generate another secure key. +## Configure Continuous Deployment -### Required Environment Variables +The included `.github/workflows/deploy.yml` workflow builds the frontend, prepares the database, and deploys the application whenever changes are pushed to `master`. You can also run it manually from the **Actions** tab. -Set the `DOMAIN` to your own domain, for example: +Log in to FastAPI Cloud and configure the [deploy token](https://fastapicloud.com/docs/advanced-features/deploy-tokens/) and application ID as GitHub repository secrets: ```bash -export DOMAIN=fastapi-project.example.com +uv run fastapi login +uv run fastapi cloud setup-ci --secrets-only --app-id ``` -The deployment Docker Compose configuration also uses `DOMAIN` to set the public frontend URL used in links generated by the backend. +If the GitHub CLI is installed and authenticated, the command configures `FASTAPI_CLOUD_TOKEN` and `FASTAPI_CLOUD_APP_ID` automatically. Otherwise, it prints the values so you can add them in your repository under **Settings** > **Secrets and variables** > **Actions**. -Set the `POSTGRES_PASSWORD` to a secure value: +The workflow runs database migrations and creates the first superuser before deploying. In the repository's **Settings** > **Secrets and variables** > **Actions** page, add these repository variables: -```bash -export POSTGRES_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" -``` +* `PROJECT_NAME` +* `FIRST_SUPERUSER` -Set the `SECRET_KEY`, used to sign tokens, to a secure value: +Add these repository secrets: -```bash -export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" -``` +* `DATABASE_URL` +* `SECRET_KEY` +* `FIRST_SUPERUSER_PASSWORD` -Set the `FIRST_SUPERUSER_PASSWORD` to a secure value: +Use the same values configured in FastAPI Cloud. For `DATABASE_URL`, use the connection URL from your database provider. The database must be reachable from GitHub-hosted runners so the preparation step can connect to it. -```bash -export FIRST_SUPERUSER_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" -``` +The deployment workflow performs these steps: -You can set several other environment variables: +1. Installs and builds the frontend into `backend/app/frontend`. +2. Runs `backend/scripts/prestart.sh` to apply database migrations and create the first superuser. +3. Deploys the project with `uv run fastapi deploy`. -* `PROJECT_NAME`: The name of the project, used in the API for the docs and emails. -* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users. -* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc). -* `SMTP_USER`: The SMTP server user to send emails. -* `SMTP_PASSWORD`: The SMTP server password to send emails. -* `EMAILS_FROM_EMAIL`: The email account to send emails from. -* `SENTRY_DSN`: The DSN for Sentry, if you are using it. +## URLs + +Replace `your-app.fastapicloud.dev` with the URL of your FastAPI Cloud application. + +Application (frontend and API): `https://your-app.fastapicloud.dev` + +Interactive API docs: `https://your-app.fastapicloud.dev/docs` + +## Docker Compose + +For deployment to your own server, see the [Docker Compose deployment guide](./deployment-docker-compose.md). ## GitHub Repository Automation @@ -91,121 +95,3 @@ Install the following GitHub Apps to enable the included repository automation: * [PR Submit](https://github.com/apps/pr-submit) lets the **Bump pre-commit hooks** and **Prepare Release** workflows create pull requests. To publish code coverage with [Smokeshow](https://github.com/samuelcolvin/smokeshow), add `SMOKESHOW_AUTH_KEY` as a repository secret. - -### Deploy with Docker Compose - -With the environment variables in place, you can deploy with Docker Compose: - -```bash -cd /root/code/app/ -docker compose -f compose.yml -f compose.deploy.yml build -docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh -docker compose -f compose.yml -f compose.deploy.yml up -d -``` - -The `compose.deploy.yml` file adds the deployment settings to the shared configuration in `compose.yml`, including HTTPS and automatic certificate handling. Explicitly listing these files also excludes the local development settings in `compose.override.yml`. - -## Continuous Deployment (CD) - -You can use GitHub Actions to deploy your project automatically. 😎 - -The included `deploy.yml` workflow deploys the application whenever changes are pushed to `master`, including when a pull request is merged. 🚀 - -### Install GitHub Actions Runner - -* On your remote server, create a user for your GitHub Actions: - -```bash -sudo adduser github -``` - -* Add Docker permissions to the `github` user: - -```bash -sudo usermod -aG docker github -``` - -* Temporarily switch to the `github` user: - -```bash -sudo su - github -``` - -* Go to the `github` user's home directory: - -```bash -cd -``` - -* [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository). - -After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost. - -To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user: - -```bash -exit -``` - -After you do it, you will be on the previous user again. And you will be on the previous directory, belonging to that user. - -Before being able to go the `github` user directory, you need to become the `root` user (you might already be): - -```bash -sudo su -``` - -* As the `root` user, go to the `actions-runner` directory inside of the `github` user's home directory: - -```bash -cd /home/github/actions-runner -``` - -* Install the self-hosted runner as a service with the user `github`: - -```bash -./svc.sh install github -``` - -* Start the service: - -```bash -./svc.sh start -``` - -* Check the status of the service: - -```bash -./svc.sh status -``` - -You can read more about it in the official guide: [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service). - -### Set Repository Secrets - -In your repository, go to **Settings** > **Secrets and variables** > **Actions** and add the following [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository): - -* `DOMAIN` -* `FIRST_SUPERUSER` -* `FIRST_SUPERUSER_PASSWORD` -* `POSTGRES_PASSWORD` -* `SECRET_KEY` - -To enable emails, configure these additional secrets with the values from your email provider: - -* `SMTP_HOST` -* `SMTP_USER` -* `SMTP_PASSWORD` -* `EMAILS_FROM_EMAIL` - -To enable Sentry, configure the `SENTRY_DSN` secret. - -## URLs - -Replace `fastapi-project.example.com` with your domain. - -Application (frontend and API): `https://fastapi-project.example.com` - -Interactive API docs: `https://fastapi-project.example.com/docs` - -Adminer: `https://adminer.fastapi-project.example.com` diff --git a/frontend/README.md b/frontend/README.md index 65e7b0c5..af7cfb6b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,6 +33,10 @@ If you are developing an API-only app and want to remove the frontend, you can d * In the `compose.override.yml` file, remove the `playwright` service. +* In the `.github/workflows/deploy.yml` file, remove the **Set up Bun**, **Install frontend dependencies**, and **Build frontend** steps. + +* In the `.fastapicloudignore` file, remove the `!backend/app/frontend/` entry. + Done, you have a frontend-less (api-only) app. 🤓 ## Generate Client From a2d2d1810870a1fe28001832de8e4fdc3a3e4940 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:16:38 +0000 Subject: [PATCH 70/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index b7298389..5bf6aee8 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Features + +* ☁️ Add FastAPI Cloud deployment. PR [#2438](https://github.com/fastapi/full-stack-fastapi-template/pull/2438) by [@tiangolo](https://github.com/tiangolo). + ## 0.11.1 (2026-08-11) ### Refactors From 7f11815e4390f5c3422d46570fb1fab99dfa0c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 12 Aug 2026 09:19:46 +0200 Subject: [PATCH 71/91] =?UTF-8?q?=F0=9F=91=B7=20Migrate=20automatic=20labe?= =?UTF-8?q?ls=20to=20Latest=20Changes=20(#2439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 25 ------------------------- .github/latest-changes.yml | 12 ++++++++++++ .github/workflows/labeler.yml | 20 -------------------- 3 files changed, 12 insertions(+), 45 deletions(-) delete mode 100644 .github/labeler.yml create mode 100644 .github/latest-changes.yml delete mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index ed657c23..00000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,25 +0,0 @@ -docs: - - all: - - changed-files: - - any-glob-to-any-file: - - '**/*.md' - - all-globs-to-all-files: - - '!frontend/**' - - '!backend/**' - - '!.github/**' - - '!scripts/**' - - '!.gitignore' - - '!.pre-commit-config.yaml' - -internal: - - all: - - changed-files: - - any-glob-to-any-file: - - .github/** - - scripts/** - - .gitignore - - .pre-commit-config.yaml - - all-globs-to-all-files: - - '!./**/*.md' - - '!frontend/**' - - '!backend/**' diff --git a/.github/latest-changes.yml b/.github/latest-changes.yml new file mode 100644 index 00000000..89034c74 --- /dev/null +++ b/.github/latest-changes.yml @@ -0,0 +1,12 @@ +auto-labels: + docs: + - exclude: frontend/** + - exclude: backend/** + - exclude: .github/** + - exclude: scripts/** + - '**/*.md' + internal: + - .github/** + - scripts/** + - .gitignore + - .pre-commit-config.yaml diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index bc990e96..00000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Labels -on: - pull_request_target: # zizmor: ignore[dangerous-triggers] - types: - - opened - - synchronize - - reopened - -permissions: {} - -jobs: - labeler: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 - - run: echo "Done adding labels" From ec86250a213d8302e0f698dae42623b8ea7cae7b Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:19:49 +0000 Subject: [PATCH 72/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 5bf6aee8..00c95503 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,10 @@ * ☁️ Add FastAPI Cloud deployment. PR [#2438](https://github.com/fastapi/full-stack-fastapi-template/pull/2438) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* 👷 Migrate automatic labels to Latest Changes. PR [#2439](https://github.com/fastapi/full-stack-fastapi-template/pull/2439) by [@tiangolo](https://github.com/tiangolo). + ## 0.11.1 (2026-08-11) ### Refactors From de2ed05b109ccd8867f6eb09ec5dc7c1bf9d0021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 12 Aug 2026 09:28:43 +0200 Subject: [PATCH 73/91] =?UTF-8?q?=F0=9F=90=B3=20Include=20frontend=20asset?= =?UTF-8?q?s=20in=20Docker=20build=20context=20(#2440)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 825db1de..c892b070 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ .git **/__pycache__ **/.venv -backend/app/frontend backend/htmlcov frontend/blob-report frontend/dist From 98fb606b57454da40eb91850e94372a9d0df483a Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:28:45 +0000 Subject: [PATCH 74/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 00c95503..0abb1102 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,10 @@ * ☁️ Add FastAPI Cloud deployment. PR [#2438](https://github.com/fastapi/full-stack-fastapi-template/pull/2438) by [@tiangolo](https://github.com/tiangolo). +### Fixes + +* 🐳 Include frontend assets in Docker build context. PR [#2440](https://github.com/fastapi/full-stack-fastapi-template/pull/2440) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 👷 Migrate automatic labels to Latest Changes. PR [#2439](https://github.com/fastapi/full-stack-fastapi-template/pull/2439) by [@tiangolo](https://github.com/tiangolo). From 84c5a9e11a9e58262599e3d772f5386924e02ca7 Mon Sep 17 00:00:00 2001 From: "pr-submit[bot]" <314742883+pr-submit[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:46:45 +0000 Subject: [PATCH 75/91] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12.0?= =?UTF-8?q?=20(#2441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pr-submit[bot] --- release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-notes.md b/release-notes.md index 0abb1102..9ee44f19 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.12.0 (2026-08-12) + ### Features * ☁️ Add FastAPI Cloud deployment. PR [#2438](https://github.com/fastapi/full-stack-fastapi-template/pull/2438) by [@tiangolo](https://github.com/tiangolo). From 42b4ca0586790d86295c04b277e7315f50ed9e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 12 Aug 2026 11:25:40 +0200 Subject: [PATCH 76/91] =?UTF-8?q?=F0=9F=93=9D=20Simplify=20project=20docum?= =?UTF-8?q?entation=20(#2442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 114 ++++++--------------------------------------- backend/README.md | 16 +++---- development.md | 69 +++++++-------------------- frontend/README.md | 25 +++++----- 4 files changed, 51 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index f12724cf..3b1ddffd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # Full Stack FastAPI Template -Test Docker Compose -Test Backend -Coverage +[![Test Docker Compose](../../actions/workflows/test-docker-compose.yml/badge.svg)](../../actions/workflows/test-docker-compose.yml) +[![Test Backend](../../actions/workflows/test-backend.yml/badge.svg)](../../actions/workflows/test-backend.yml) ## Technology Stack and Features @@ -15,128 +14,41 @@ - 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack. - 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components. - 🤖 An automatically generated frontend client. - - 🧪 [Playwright](https://playwright.dev) for End-to-End testing. + - 🧪 [Playwright](https://playwright.dev) for end-to-end testing. - 🦇 Dark mode support. - ☁️ [FastAPI Cloud](https://fastapicloud.com) for deployment. - 🐋 [Docker Compose](https://www.docker.com) for local services and self-hosted deployment. - 📞 [Traefik](https://traefik.io) as a reverse proxy with automatic HTTPS. - 🔒 Secure password hashing by default. - 🔑 JWT (JSON Web Token) authentication. -- 📫 Email based password recovery. +- 📫 Email-based password recovery. - 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development. - ✅ Tests with [Pytest](https://pytest.org). -- 🚢 Deployment with FastAPI Cloud or self-hosting with Docker Compose and Traefik. - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. ### Dashboard Login -[![Dashboard login screenshot](img/login.png)](https://github.com/fastapi/full-stack-fastapi-template) +![Dashboard login screenshot](img/login.png) ### Dashboard - Admin -[![Admin dashboard screenshot](img/dashboard.png)](https://github.com/fastapi/full-stack-fastapi-template) +![Admin dashboard screenshot](img/dashboard.png) ### Dashboard - Items -[![Items dashboard screenshot](img/dashboard-items.png)](https://github.com/fastapi/full-stack-fastapi-template) +![Items dashboard screenshot](img/dashboard-items.png) ### Dashboard - Dark Mode -[![Dark mode dashboard screenshot](img/dashboard-dark.png)](https://github.com/fastapi/full-stack-fastapi-template) +![Dark mode dashboard screenshot](img/dashboard-dark.png) ### Interactive API Documentation -[![API docs](img/docs.png)](https://github.com/fastapi/full-stack-fastapi-template) +![API docs](img/docs.png) -## How To Use It +## How to Use It -You can **just fork or clone** this repository and use it as is. - -✨ It just works. ✨ - -### How to Use a Private Repository - -If you want to have a private repository, GitHub won't allow you to simply fork it as it doesn't allow changing the visibility of forks. - -But you can do the following: - -- Create a new GitHub repo, for example `my-full-stack`. -- Clone this repository manually, set the name with the name of the project you want to use, for example `my-full-stack`: - -```bash -git clone git@github.com:fastapi/full-stack-fastapi-template.git my-full-stack -``` - -- Enter into the new directory: - -```bash -cd my-full-stack -``` - -- Set the new origin to your new repository, copy it from the GitHub interface, for example: - -```bash -git remote set-url origin git@github.com:octocat/my-full-stack.git -``` - -- Add this repo as another "remote" to allow you to get updates later: - -```bash -git remote add upstream git@github.com:fastapi/full-stack-fastapi-template.git -``` - -- Push the code to your new repository: - -```bash -git push -u origin master -``` - -### Update From the Original Template - -After cloning the repository, and after doing changes, you might want to get the latest changes from this original template. - -- Make sure you added the original repository as a remote, you can check it with: - -```bash -git remote -v - -origin git@github.com:octocat/my-full-stack.git (fetch) -origin git@github.com:octocat/my-full-stack.git (push) -upstream git@github.com:fastapi/full-stack-fastapi-template.git (fetch) -upstream git@github.com:fastapi/full-stack-fastapi-template.git (push) -``` - -- Pull the latest changes without merging: - -```bash -git pull --no-commit upstream master -``` - -This will download the latest changes from this template without committing them, that way you can check everything is right before committing. - -- If there are conflicts, solve them in your editor. - -- Once you are done, commit the changes: - -```bash -git merge --continue -``` - -### Configure - -You can update the local settings in the `.env` file. Before deploying, configure the environment variables and secrets described in the [FastAPI Cloud deployment guide](./deployment.md) or the [Docker Compose deployment guide](./deployment-docker-compose.md). Never deploy with the default `changethis` values. - -### Generate Secret Keys - -Some environment variables in the `.env` file have a default value of `changethis`. - -You have to change them with a secret key, to generate secret keys you can run the following command: - -```bash -python -c "import secrets; print(secrets.token_urlsafe(32))" -``` - -Copy the content and use that as password / secret key. And run that again to generate another secure key. +Click the **Use this template** button at the top of this page to create a new repository. ## Backend Development @@ -148,9 +60,9 @@ Frontend docs: [frontend/README.md](./frontend/README.md). ## Deployment -Deployment docs: [deployment.md](./deployment.md). +FastAPI Cloud deployment: [deployment.md](./deployment.md). -Docker Compose deployment: [deployment-docker-compose.md](./deployment-docker-compose.md). +Self-hosted deployment with Docker Compose: [deployment-docker-compose.md](./deployment-docker-compose.md). ## Development diff --git a/backend/README.md b/backend/README.md index f4536194..c8605ffb 100644 --- a/backend/README.md +++ b/backend/README.md @@ -58,19 +58,19 @@ To open a shell in the backend container: $ docker compose exec backend bash ``` -## Backend tests +## Backend Tests To test the backend from the `backend` directory, run: ```console -$ uv run bash ./scripts/test.sh +$ uv run bash scripts/test.sh ``` -The tests run with Pytest, modify and add tests to `./backend/tests/`. +The tests run with Pytest. Modify existing tests or add new ones in `./backend/tests/`. -If you use GitHub Actions the tests will run automatically. +If you use GitHub Actions, the tests will run automatically. -### Test running stack +### Test a Running Stack If your stack is already up and you just want to run the tests, you can use: @@ -78,7 +78,7 @@ If your stack is already up and you just want to run the tests, you can use: docker compose exec backend bash scripts/tests-start.sh ``` -That `/app/scripts/tests-start.sh` script just calls `pytest` after making sure that the rest of the stack is running. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded. +The `/app/backend/scripts/tests-start.sh` script calls `pytest` after making sure that the rest of the stack is running. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded. For example, to stop on first error: @@ -88,7 +88,7 @@ docker compose exec backend bash scripts/tests-start.sh -x ### Test Coverage -When the tests are run, a file `htmlcov/index.html` is generated, you can open it in your browser to see the coverage of the tests. +When the tests run, they generate `htmlcov/index.html`. Open it in your browser to inspect the test coverage. ## Migrations @@ -128,7 +128,7 @@ If you don't want to start with the default models and want to remove them / mod The email templates are written with [React Email](https://react.email) in `./packages/react-email/`. The `emails` directory holds one component per email and the `ui` directory holds the shared components (layout, heading, button, link, callout). -The rendered HTML in `./backend/app/email-templates/` is generated from those components, it is what the application sends, and it shouldn't be edited by hand. +The rendered HTML in `./backend/app/email-templates/` is generated from those components. It is what the application sends and should not be edited by hand. To preview the emails while editing them, start the dev server from the root of the project: diff --git a/development.md b/development.md index 07eb0491..2cce9d12 100644 --- a/development.md +++ b/development.md @@ -81,11 +81,11 @@ Stop a locally running FastAPI server before starting the Compose backend becaus Mailcatcher captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailcatcher` service. Captured emails are available at . -## Docker Compose files and env vars +## Docker Compose Files and Environment Variables -There is a main `compose.yml` file with all the configurations that apply to the whole stack, it is used automatically by `docker compose`. +The main `compose.yml` file contains the configuration shared by the whole stack. Docker Compose loads it automatically. -And there's also a `compose.override.yml` with overrides for development, for example to mount the source code as a volume. It is used automatically by `docker compose` to apply overrides on top of `compose.yml`. +The `compose.override.yml` file adds local development settings, such as mounting the source code as a volume. Docker Compose also loads it automatically and applies it on top of `compose.yml`. The `compose.deploy.yml` file contains the deployment-specific settings, including HTTPS and automatic certificate handling. It is explicitly combined with `compose.yml` when deploying the application. @@ -97,79 +97,42 @@ After changing variables, make sure you restart the stack: docker compose watch ``` -## The .env file +## The `.env` File -The `.env` file contains the shared local defaults, generated keys, passwords, and other configuration. Its hostnames use `localhost` for processes running on your machine. Docker Compose overrides hostnames such as the database and SMTP server with their Compose service names. +The tracked `.env` file contains local development defaults, passwords, and other configuration. Its hostnames use `localhost` for processes running on your machine. Docker Compose overrides hostnames such as the database and SMTP server with their Compose service names. -Depending on your workflow, you could want to exclude it from Git, for example if your project is public. In that case, you would have to make sure to set up a way for your CI tools to obtain it while building or deploying your project. +Do not store deployment secrets in `.env`. Configure them as described in the [FastAPI Cloud deployment guide](./deployment.md) or the [Docker Compose deployment guide](./deployment-docker-compose.md). -One way to do it could be to add each environment variable to your CI/CD system. +## Pre-commit Hooks and Code Linting -## Pre-commits and code linting - -we are using a tool called [prek](https://prek.j178.dev/) (modern alternative to [Pre-commit](https://pre-commit.com/)) for code linting and formatting. - -When you install it, it runs right before making a commit in git. This way it ensures that the code is consistent and formatted even before it is committed. +The project uses [prek](https://prek.j178.dev/), a modern alternative to [pre-commit](https://pre-commit.com/), for code linting and formatting. You can find a file `.pre-commit-config.yaml` with configurations at the root of the project. -#### Install prek to run automatically +### Install `prek` to Run Automatically `prek` is already part of the dependencies of the project. -After having the `prek` tool installed and available, you need to "install" it in the local repository, so that it runs automatically before each commit. - -Using `uv`, you could do it with (make sure you are inside `backend` folder): +From the project root, install the Git hook so that `prek` runs automatically before each commit: ```bash -❯ uv run prek install -f -prek installed at `../.git/hooks/pre-commit` +uv run prek install -f ``` The `-f` flag forces the installation, in case there was already a `pre-commit` hook previously installed. -Now whenever you try to commit, e.g. with: +Now whenever you try to commit, for example with: ```bash git commit ``` -...prek will run and check and format the code you are about to commit, and will ask you to add that code (stage it) with git again before committing. +`prek` will check and format the code you are about to commit. If it modifies any files, add those files to Git again before committing. -Then you can `git add` the modified/fixed files again and now you can commit. +### Run `prek` Manually -#### Running prek hooks manually - -you can also run `prek` manually on all the files, you can do it using `uv` with: +You can also run `prek` manually on all files from the project root: ```bash -❯ uv run prek run --all-files -check for added large files..............................................Passed -check toml...............................................................Passed -check yaml...............................................................Passed -fix end of files.........................................................Passed -trim trailing whitespace.................................................Passed -ruff.....................................................................Passed -ruff-format..............................................................Passed -biome check..............................................................Passed +uv run prek run --all-files ``` - -## URLs - -The deployed URLs use these same paths, but with your own domain. - -### Development URLs - -Development URLs, for local development. - -Application: - -Automatic Interactive Docs (Swagger UI): - -Automatic Alternative Docs (ReDoc): - -Adminer: - -Traefik UI: - -MailCatcher: diff --git a/frontend/README.md b/frontend/README.md index af7cfb6b..5055e35c 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,27 +1,29 @@ # FastAPI Project - Frontend -The frontend is built with [Vite](https://vitejs.dev/), [React](https://reactjs.org/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router) and [Tailwind CSS](https://tailwindcss.com/). +The frontend is built with [Vite](https://vitejs.dev/), [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router), [Tailwind CSS](https://tailwindcss.com/), and [shadcn/ui](https://ui.shadcn.com/). ## Requirements -- [Bun](https://bun.sh/) (recommended) or [Node.js](https://nodejs.org/) +- [Bun](https://bun.sh/) ## Quick Start +From the project root, install the dependencies and start the frontend development server: + ```bash bun install bun run dev ``` -* Then open your browser at http://localhost:5173/. +Then open in your browser. Run `uv run bash scripts/prestart.sh` and `uv run fastapi dev` from the `backend` directory, with PostgreSQL running in Docker Compose. See [../development.md](../development.md) for the complete setup. To serve the frontend with FastAPI, run `bun run build` from the `frontend` directory and open `http://localhost:8000`. -Check the file `package.json` to see other available options. +Check `frontend/package.json` to see the other available commands. -### Removing the frontend +## Removing the Frontend If you are developing an API-only app and want to remove the frontend, you can do it easily: @@ -37,13 +39,13 @@ If you are developing an API-only app and want to remove the frontend, you can d * In the `.fastapicloudignore` file, remove the `!backend/app/frontend/` entry. -Done, you have a frontend-less (api-only) app. 🤓 +Done, you now have an API-only app. 🤓 ## Generate Client ### Automatically -* From the top level project directory, run the script: +* From the project root, run the script: ```bash bash ./scripts/generate-client.sh @@ -65,7 +67,7 @@ bun run generate-client * Commit the changes. -Notice that everytime the backend changes (changing the OpenAPI schema), you should follow these steps again to update the frontend client. +Regenerate the client whenever backend changes affect the OpenAPI schema. ## Using a Remote API @@ -82,11 +84,12 @@ Then, when you run the frontend, it will use that URL as the base URL for the AP The frontend code is structured as follows: * `frontend/src` - The main frontend code. -* `frontend/src/assets` - Static assets. +* `frontend/public` - Static assets. * `frontend/src/client` - The generated OpenAPI client. -* `frontend/src/components` - The different components of the frontend. +* `frontend/src/components` - The components of the frontend, including the shadcn/ui components in `frontend/src/components/ui`. * `frontend/src/hooks` - Custom hooks. -* `frontend/src/routes` - The different routes of the frontend which include the pages. +* `frontend/src/lib` - Shared frontend utilities. +* `frontend/src/routes` - The frontend routes and pages. ## End-to-End Testing with Playwright From c350936d2888ef16ff4f5549684fd8db54935a89 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:25:43 +0000 Subject: [PATCH 77/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 9ee44f19..e8b78bbf 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* 📝 Simplify project documentation. PR [#2442](https://github.com/fastapi/full-stack-fastapi-template/pull/2442) by [@tiangolo](https://github.com/tiangolo). + ## 0.12.0 (2026-08-12) ### Features From d95518294e6b73428b363bf502e13cb086e6e6cd Mon Sep 17 00:00:00 2001 From: Ide Date: Fri, 14 Aug 2026 19:22:05 +0200 Subject: [PATCH 78/91] =?UTF-8?q?=F0=9F=90=9B=20Add=20background=20to=20th?= =?UTF-8?q?e=20sticky=20layout=20header=20(#2384)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- frontend/src/routes/_layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/_layout.tsx b/frontend/src/routes/_layout.tsx index 38d9f467..7bafdf72 100644 --- a/frontend/src/routes/_layout.tsx +++ b/frontend/src/routes/_layout.tsx @@ -25,7 +25,7 @@ function Layout() { -
+
From 75b402644329e53b2c9c862181b47003ad5fc914 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:22:11 +0000 Subject: [PATCH 79/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index e8b78bbf..c82b2b3a 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Add background to the sticky layout header. PR [#2384](https://github.com/fastapi/full-stack-fastapi-template/pull/2384) by [@istoutjesdijk](https://github.com/istoutjesdijk). + ### Docs * 📝 Simplify project documentation. PR [#2442](https://github.com/fastapi/full-stack-fastapi-template/pull/2442) by [@tiangolo](https://github.com/tiangolo). From 981ae061b442cdd77ed410a1c42aa8a7f079729e Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:38:38 +0200 Subject: [PATCH 80/91] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Replace=20Mailcatche?= =?UTF-8?q?r=20with=20Mailpit=20for=20local=20email=20testing=20(#2436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-backend.yml | 2 +- README.md | 2 +- backend/README.md | 4 +- compose.override.yml | 12 +++--- development.md | 12 +++--- frontend/.env | 2 +- frontend/tests/reset-password.spec.ts | 14 +++--- frontend/tests/utils/mailcatcher.ts | 62 --------------------------- frontend/tests/utils/mailpit.ts | 59 +++++++++++++++++++++++++ 9 files changed, 81 insertions(+), 88 deletions(-) delete mode 100644 frontend/tests/utils/mailcatcher.ts create mode 100644 frontend/tests/utils/mailpit.ts diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 93c169a9..92c5fc98 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -28,7 +28,7 @@ jobs: # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 version: "0.11.18" - run: docker compose down -v --remove-orphans - - run: docker compose up -d db mailcatcher + - run: docker compose up -d db mailpit - name: Migrate DB run: uv run bash scripts/prestart.sh working-directory: backend diff --git a/README.md b/README.md index 3b1ddffd..a3e20f4d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ - 🔒 Secure password hashing by default. - 🔑 JWT (JSON Web Token) authentication. - 📫 Email-based password recovery. -- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development. +- 📬 [Mailpit](https://mailpit.axllent.org) for local email testing during development. - ✅ Tests with [Pytest](https://pytest.org). - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. diff --git a/backend/README.md b/backend/README.md index c8605ffb..92175a0b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -9,10 +9,10 @@ Run the backend locally and connect it to PostgreSQL in Docker Compose. -From the project root, start PostgreSQL and Mailcatcher: +From the project root, start PostgreSQL and Mailpit: ```console -$ docker compose up -d db mailcatcher +$ docker compose up -d db mailpit ``` Then, from `./backend/`, install the dependencies, prepare the database, and start the development server: diff --git a/compose.override.yml b/compose.override.yml index 05b98090..ec0142b1 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -64,14 +64,14 @@ services: - ./backend/htmlcov:/app/backend/htmlcov environment: FASTAPI_ENV: "development" - SMTP_HOST: "mailcatcher" + SMTP_HOST: "mailpit" SMTP_PORT: "1025" SMTP_TLS: "false" - mailcatcher: - image: schickling/mailcatcher + mailpit: + image: axllent/mailpit ports: - - "1080:1080" + - "8025:8025" - "1025:1025" playwright: @@ -83,13 +83,13 @@ services: ipc: host depends_on: - backend - - mailcatcher + - mailpit environment: - FIRST_SUPERUSER=${FIRST_SUPERUSER:?Variable not set} - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD:?Variable not set} - PLAYWRIGHT_BASE_URL=http://backend:8000 - VITE_API_URL=http://backend:8000 - - MAILCATCHER_HOST=http://mailcatcher:1080 + - MAILPIT_HOST=http://mailpit:8025 # For the reports when run locally - PLAYWRIGHT_HTML_HOST=0.0.0.0 - CI=${CI:-} diff --git a/development.md b/development.md index 2cce9d12..efd21b2f 100644 --- a/development.md +++ b/development.md @@ -2,12 +2,12 @@ ## Local Development -For local development, run PostgreSQL and Mailcatcher with Docker Compose, and run the FastAPI and Vite development servers locally. +For local development, run PostgreSQL and Mailpit with Docker Compose, and run the FastAPI and Vite development servers locally. Start the supporting services: ```bash -docker compose up -d db mailcatcher +docker compose up -d db mailpit ``` Then, from the `backend` directory, install the dependencies and prepare the database: @@ -38,7 +38,7 @@ Backend API: Automatic interactive API documentation with Swagger UI: -Mailcatcher: +Mailpit: The frontend development server uses the backend at `http://localhost:8000`, as configured in `frontend/.env`. @@ -71,15 +71,15 @@ Adminer, database web administration: Traefik UI, to see how the routes are being handled by the proxy: -Mailcatcher: +Mailpit: Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`. **Note**: The first time you start the stack, it might take a minute for all the services to be ready. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service. -## Mailcatcher +## Mailpit -Mailcatcher captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailcatcher` service. Captured emails are available at . +[Mailpit](https://mailpit.axllent.org) captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailpit` service. Captured emails are available at . ## Docker Compose Files and Environment Variables diff --git a/frontend/.env b/frontend/.env index 27fcbfe8..35d97648 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,2 +1,2 @@ VITE_API_URL=http://localhost:8000 -MAILCATCHER_HOST=http://localhost:1080 +MAILPIT_HOST=http://localhost:8025 diff --git a/frontend/tests/reset-password.spec.ts b/frontend/tests/reset-password.spec.ts index e0731d49..fa7be461 100644 --- a/frontend/tests/reset-password.spec.ts +++ b/frontend/tests/reset-password.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test" -import { findLastEmail } from "./utils/mailcatcher" +import { emailUrl, findLastEmail } from "./utils/mailpit" import { randomEmail, randomPassword } from "./utils/random" import { logInUser, signUpNewUser } from "./utils/user" @@ -46,13 +46,11 @@ test("User can reset password successfully using the link", async ({ const emailData = await findLastEmail({ request, - filter: (e) => e.recipients.includes(`<${email}>`), + query: `to:${email}`, timeout: 5000, }) - await page.goto( - `${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`, - ) + await page.goto(emailUrl(emailData)) const selector = 'a[href*="/reset-password?token="]' @@ -99,13 +97,11 @@ test("Weak new password validation", async ({ page, request }) => { const emailData = await findLastEmail({ request, - filter: (e) => e.recipients.includes(`<${email}>`), + query: `to:${email}`, timeout: 5000, }) - await page.goto( - `${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`, - ) + await page.goto(emailUrl(emailData)) const selector = 'a[href*="/reset-password?token="]' const url = await page.getAttribute(selector, "href") diff --git a/frontend/tests/utils/mailcatcher.ts b/frontend/tests/utils/mailcatcher.ts deleted file mode 100644 index 8e6f78b2..00000000 --- a/frontend/tests/utils/mailcatcher.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { APIRequestContext } from "@playwright/test" - -type Email = { - id: number - recipients: string[] - subject: string -} - -async function findEmail({ - request, - filter, -}: { - request: APIRequestContext - filter?: (email: Email) => boolean -}) { - const response = await request.get(`${process.env.MAILCATCHER_HOST}/messages`) - - let emails = await response.json() - - if (filter) { - emails = emails.filter(filter) - } - - const email = emails[emails.length - 1] - - if (email) { - return email as Email - } - - return null -} - -export function findLastEmail({ - request, - filter, - timeout = 5000, -}: { - request: APIRequestContext - filter?: (email: Email) => boolean - timeout?: number -}) { - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => reject(new Error("Timeout while trying to get latest email")), - timeout, - ), - ) - - const checkEmails = async () => { - while (true) { - const emailData = await findEmail({ request, filter }) - - if (emailData) { - return emailData - } - // Wait for 100ms before checking again - await new Promise((resolve) => setTimeout(resolve, 100)) - } - } - - return Promise.race([timeoutPromise, checkEmails()]) -} diff --git a/frontend/tests/utils/mailpit.ts b/frontend/tests/utils/mailpit.ts new file mode 100644 index 00000000..92c02e28 --- /dev/null +++ b/frontend/tests/utils/mailpit.ts @@ -0,0 +1,59 @@ +import type { APIRequestContext } from "@playwright/test" + +type Address = { + Name: string + Address: string +} + +type Email = { + ID: string + To: Address[] + Subject: string +} + +async function findEmail({ + request, + query, +}: { + request: APIRequestContext + query: string +}) { + const response = await request.get( + `${process.env.MAILPIT_HOST}/api/v1/search`, + { + params: { query, limit: 1 }, + }, + ) + + const { messages }: { messages: Email[] } = await response.json() + + return messages[0] ?? null +} + +export async function findLastEmail({ + request, + query, + timeout = 5000, +}: { + request: APIRequestContext + query: string + timeout?: number +}) { + const deadline = Date.now() + timeout + + while (Date.now() < deadline) { + const email = await findEmail({ request, query }) + + if (email) { + return email + } + + await new Promise((resolve) => setTimeout(resolve, 100)) + } + + throw new Error(`Timeout while trying to get the latest email for "${query}"`) +} + +export function emailUrl(email: Email) { + return `${process.env.MAILPIT_HOST}/view/${email.ID}.html` +} From 162344da111e833b30892728372ab95331f06873 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:38:41 +0000 Subject: [PATCH 81/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index c82b2b3a..645fde73 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,10 @@ * 🐛 Add background to the sticky layout header. PR [#2384](https://github.com/fastapi/full-stack-fastapi-template/pull/2384) by [@istoutjesdijk](https://github.com/istoutjesdijk). +### Refactors + +* ♻️ Replace Mailcatcher with Mailpit for local email testing. PR [#2436](https://github.com/fastapi/full-stack-fastapi-template/pull/2436) by [@alejsdev](https://github.com/alejsdev). + ### Docs * 📝 Simplify project documentation. PR [#2442](https://github.com/fastapi/full-stack-fastapi-template/pull/2442) by [@tiangolo](https://github.com/tiangolo). From f369a27746407743b81fce316090e3050ea611a2 Mon Sep 17 00:00:00 2001 From: "Chris (BS-1495)" <162133545+chris-bs23@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:09:31 +0600 Subject: [PATCH 82/91] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Sonner=20toast=20the?= =?UTF-8?q?me=20sync=20(#2450)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 3 --- frontend/package.json | 1 - frontend/src/components/ui/sonner.tsx | 6 +++--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index dea386f2..f4820082 100644 --- a/bun.lock +++ b/bun.lock @@ -33,7 +33,6 @@ "clsx": "^2.1.1", "form-data": "4.0.6", "lucide-react": "^1.28.0", - "next-themes": "^0.4.6", "react": "^19.2.8", "react-dom": "^19.2.8", "react-error-boundary": "^6.1.2", @@ -844,8 +843,6 @@ "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], - "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], diff --git a/frontend/package.json b/frontend/package.json index 9a67018b..c3aa60de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,6 @@ "clsx": "^2.1.1", "form-data": "4.0.6", "lucide-react": "^1.28.0", - "next-themes": "^0.4.6", "react": "^19.2.8", "react-dom": "^19.2.8", "react-error-boundary": "^6.1.2", diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx index 9b20afe2..b9f32eb3 100644 --- a/frontend/src/components/ui/sonner.tsx +++ b/frontend/src/components/ui/sonner.tsx @@ -7,15 +7,15 @@ import { OctagonXIcon, TriangleAlertIcon, } from "lucide-react" -import { useTheme } from "next-themes" +import { useTheme } from "@/components/theme-provider" import { Toaster as Sonner, type ToasterProps } from "sonner" const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() + const { theme } = useTheme() return ( , From 2734dfd002843a50772a44bc8f8d50e65898182e Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:09:34 +0000 Subject: [PATCH 83/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 645fde73..03f7020f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ ### Fixes +* 🐛 Fix Sonner toast theme sync. PR [#2450](https://github.com/fastapi/full-stack-fastapi-template/pull/2450) by [@chris-bs23](https://github.com/chris-bs23). * 🐛 Add background to the sticky layout header. PR [#2384](https://github.com/fastapi/full-stack-fastapi-template/pull/2384) by [@istoutjesdijk](https://github.com/istoutjesdijk). ### Refactors From 00a6aa978e467cb6f39bc90bbb0b2d61e909274f Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:09:48 +0200 Subject: [PATCH 84/91] =?UTF-8?q?=F0=9F=92=84=20Refactor=20email=20templat?= =?UTF-8?q?es=20and=20components=20for=20improved=20styling=20(#2451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/email-templates/new_account.html | 2 +- .../app/email-templates/reset_password.html | 2 +- backend/app/email-templates/test_email.html | 2 +- packages/react-email/emails/new_account.tsx | 24 ++- .../react-email/emails/reset_password.tsx | 24 ++- packages/react-email/emails/test_email.tsx | 20 ++- packages/react-email/ui/Button.tsx | 19 ++- packages/react-email/ui/Callout.tsx | 44 ++++-- packages/react-email/ui/Heading.tsx | 14 +- packages/react-email/ui/Layout.tsx | 146 +++++++++++------- packages/react-email/ui/Link.tsx | 12 +- 11 files changed, 213 insertions(+), 96 deletions(-) diff --git a/backend/app/email-templates/new_account.html b/backend/app/email-templates/new_account.html index 1368a128..bbff20e7 100644 --- a/backend/app/email-templates/new_account.html +++ b/backend/app/email-templates/new_account.html @@ -1 +1 @@ -{{ project_name }} - New account
Your {{ project_name }} account is ready
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Welcome to {{ project_name }}!

Hi,

Your account has been successfully created and is ready to use. These are your credentials:

Username{{ username }}

Password{{ password }}

Get started by signing in to your dashboard:

Go to Dashboard

Or copy and paste this link into your browser:
{{ link }}

For security reasons, change your password after your first sign in.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file +{{ project_name }} - New account
Your {{ project_name }} account is ready
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Welcome to {{ project_name }}!

Hi,

Your account has been successfully created and is ready to use. These are your credentials:

Username
{{ username }}

Password
{{ password }}

Get started by signing in to your dashboard:

Go to Dashboard

Or copy and paste this link into your browser:
{{ link }}

For security reasons, change your password after your first sign in.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file diff --git a/backend/app/email-templates/reset_password.html b/backend/app/email-templates/reset_password.html index 5b4d76bc..134ddd34 100644 --- a/backend/app/email-templates/reset_password.html +++ b/backend/app/email-templates/reset_password.html @@ -1 +1 @@ -{{ project_name }} - Password recovery
Reset your {{ project_name }} password
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Reset your password

Hi {{ username }},

We've received a request to reset the password for your {{ project_name }} account. Choose a new one by clicking the button below:

Reset password

Or copy and paste this link into your browser:
{{ link }}

This link will expire in {{ valid_hours }} hours.

If you didn't request a password recovery, you can safely ignore this email.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file +{{ project_name }} - Password recovery
Reset your {{ project_name }} password
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Reset your password

Hi {{ username }},

We've received a request to reset the password for your {{ project_name }} account. Choose a new one by clicking the button below:

Reset password

Or copy and paste this link into your browser:
{{ link }}

This link will expire in {{ valid_hours }} hours.

If you didn't request a password recovery, you can safely ignore this email.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file diff --git a/backend/app/email-templates/test_email.html b/backend/app/email-templates/test_email.html index d0fe62fe..b2792c22 100644 --- a/backend/app/email-templates/test_email.html +++ b/backend/app/email-templates/test_email.html @@ -1 +1 @@ -{{ project_name }} - Test email
{{ project_name }} email delivery is working
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Test email

Hi,

This is a test email from {{ project_name }}. If you're reading it, email delivery is configured correctly.

Sent to{{ email }}

If you weren't expecting this email, you can safely ignore it.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file +{{ project_name }} - Test email
{{ project_name }} email delivery is working
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

{{ project_name }}

Test email

Hi,

This is a test email from {{ project_name }}. If you're reading it, email delivery is configured correctly.

Sent to
{{ email }}

If you weren't expecting this email, you can safely ignore it.


© 2026 {{ project_name }}. All rights reserved.

\ No newline at end of file diff --git a/packages/react-email/emails/new_account.tsx b/packages/react-email/emails/new_account.tsx index aae6f240..127d395e 100644 --- a/packages/react-email/emails/new_account.tsx +++ b/packages/react-email/emails/new_account.tsx @@ -25,8 +25,8 @@ export default function NewAccount({ project_name={project_name} > Welcome to {project_name}! - Hi, - + Hi, + Your account has been successfully created and is ready to use. These are your credentials: @@ -34,18 +34,32 @@ export default function NewAccount({ - + Get started by signing in to your dashboard: Go to Dashboard - + Or copy and paste this link into your browser:
{link}
- + For security reasons, change your password after your first sign in. ) } + +const bodyTextStyle = { + color: "#334155", + fontSize: "15px", + lineHeight: "26px", + margin: "0 0 18px", +} + +const supportingTextStyle = { + color: "#64748b", + fontSize: "14px", + lineHeight: "23px", + margin: "0 0 16px", +} diff --git a/packages/react-email/emails/reset_password.tsx b/packages/react-email/emails/reset_password.tsx index 2dba207c..184901e0 100644 --- a/packages/react-email/emails/reset_password.tsx +++ b/packages/react-email/emails/reset_password.tsx @@ -24,24 +24,38 @@ export default function ResetPassword({ project_name={project_name} > Reset your password - Hi {username}, - + Hi {username}, + We've received a request to reset the password for your {project_name}{" "} account. Choose a new one by clicking the button below: Reset password - + Or copy and paste this link into your browser:
{link}
- + This link will expire in {valid_hours} hours. - + If you didn't request a password recovery, you can safely ignore this email. ) } + +const bodyTextStyle = { + color: "#334155", + fontSize: "15px", + lineHeight: "26px", + margin: "0 0 18px", +} + +const supportingTextStyle = { + color: "#64748b", + fontSize: "14px", + lineHeight: "23px", + margin: "0 0 16px", +} diff --git a/packages/react-email/emails/test_email.tsx b/packages/react-email/emails/test_email.tsx index b3914287..3c4403d9 100644 --- a/packages/react-email/emails/test_email.tsx +++ b/packages/react-email/emails/test_email.tsx @@ -19,17 +19,31 @@ export default function TestEmail({ project_name={project_name} > Test email - Hi, - + Hi, + This is a test email from {project_name}. If you're reading it, email delivery is configured correctly. - + If you weren't expecting this email, you can safely ignore it. ) } + +const bodyTextStyle = { + color: "#334155", + fontSize: "15px", + lineHeight: "26px", + margin: "0 0 18px", +} + +const supportingTextStyle = { + color: "#64748b", + fontSize: "14px", + lineHeight: "23px", + margin: "0 0 16px", +} diff --git a/packages/react-email/ui/Button.tsx b/packages/react-email/ui/Button.tsx index fff4a68d..0f55a5bc 100644 --- a/packages/react-email/ui/Button.tsx +++ b/packages/react-email/ui/Button.tsx @@ -3,10 +3,21 @@ import { Button } from "@react-email/components" type ButtonProps = React.PropsWithChildren & { href: string } export const LinkButton = ({ children, href }: ButtonProps) => ( - ) + +const buttonStyle = { + backgroundColor: "#00796b", + borderRadius: "6px", + color: "#ffffff", + display: "inline-block", + fontSize: "14px", + fontWeight: "700", + lineHeight: "20px", + margin: "8px 0 28px", + padding: "13px 28px", + textAlign: "center" as const, + textDecoration: "none", +} diff --git a/packages/react-email/ui/Callout.tsx b/packages/react-email/ui/Callout.tsx index 60a96d7d..8dd03d82 100644 --- a/packages/react-email/ui/Callout.tsx +++ b/packages/react-email/ui/Callout.tsx @@ -3,20 +3,44 @@ import { Section, Text } from "@react-email/components" type CalloutProps = React.PropsWithChildren export const Callout = ({ children }: CalloutProps) => ( -
- {children} -
+
{children}
) type DetailProps = { label: string; value: string } export const Detail = ({ label, value }: DetailProps) => ( - - - {label} - - - {value} - + + {label} +
+ {value}
) + +const calloutStyle = { + backgroundColor: "#f2f8f7", + borderLeft: "3px solid #00897b", + margin: "24px 0 28px", + padding: "6px 18px", +} + +const detailStyle = { + margin: "12px 0", +} + +const labelStyle = { + color: "#64748b", + fontSize: "11px", + fontWeight: "700", + letterSpacing: "1px", + lineHeight: "16px", + textTransform: "uppercase" as const, +} + +const valueStyle = { + color: "#1e293b", + fontFamily: 'Consolas, "Courier New", monospace', + fontSize: "14px", + fontWeight: "700", + lineHeight: "22px", + wordBreak: "break-all" as const, +} diff --git a/packages/react-email/ui/Heading.tsx b/packages/react-email/ui/Heading.tsx index 6f8565c4..8ccb3ebe 100644 --- a/packages/react-email/ui/Heading.tsx +++ b/packages/react-email/ui/Heading.tsx @@ -3,10 +3,16 @@ import { Heading as BaseHeading } from "@react-email/components" type HeadingProps = React.PropsWithChildren export const Heading = ({ children }: HeadingProps) => ( - + {children} ) + +const headingStyle = { + color: "#17212b", + fontSize: "26px", + fontWeight: "700", + letterSpacing: "-0.3px", + lineHeight: "34px", + margin: "0 0 20px", +} diff --git a/packages/react-email/ui/Layout.tsx b/packages/react-email/ui/Layout.tsx index a4cc1fe9..655450bd 100644 --- a/packages/react-email/ui/Layout.tsx +++ b/packages/react-email/ui/Layout.tsx @@ -1,11 +1,13 @@ import { Body, + Column, + Container, Head, Hr, Html, Preview, + Row, Section, - Tailwind, Text, } from "@react-email/components" @@ -22,64 +24,88 @@ export const Layout = ({ children, }: LayoutProps) => { return ( - - - - {title} - - - {preview ? {preview} : null} - -
-
- - {project_name} - -
-
{children}
-
-
- - © {new Date().getFullYear()} {project_name}. All rights reserved. - -
- - -
+ + + {title} + + + {preview ? {preview} : null} + + + + +
+
+ {project_name} +
+
{children}
+
+
+ + © {new Date().getFullYear()} {project_name}. All rights + reserved. + +
+
+
+
+ + ) } + +const fontFamily = + '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif' + +const bodyStyle = { + backgroundColor: "#f4f7f6", + color: "#1e293b", + fontFamily, + margin: "0", + padding: "32px 12px", +} + +const containerStyle = { + backgroundColor: "#ffffff", + border: "1px solid #dce7e5", + borderRadius: "8px", + boxSizing: "border-box" as const, + margin: "0 auto", + maxWidth: "560px", + width: "100%", +} + +const contentStyle = { + padding: "40px 36px 32px", +} + +const brandRuleStyle = { + border: "0", + borderTop: "3px solid #00897b", + margin: "0 0 16px", + width: "32px", +} + +const brandStyle = { + color: "#334155", + fontSize: "13px", + fontWeight: "700", + letterSpacing: "1px", + lineHeight: "20px", + margin: "0 0 32px", + textTransform: "uppercase" as const, +} + +const footerRuleStyle = { + border: "0", + borderTop: "1px solid #dce7e5", + margin: "36px 0 18px", + width: "100%", +} + +const footerStyle = { + color: "#64748b", + fontSize: "12px", + lineHeight: "20px", + margin: "0", +} diff --git a/packages/react-email/ui/Link.tsx b/packages/react-email/ui/Link.tsx index b2011df8..dcb28c79 100644 --- a/packages/react-email/ui/Link.tsx +++ b/packages/react-email/ui/Link.tsx @@ -1,7 +1,15 @@ +import { Link as EmailLink } from "@react-email/components" + type LinkProps = React.PropsWithChildren & { href: string } export const Link = ({ children, href }: LinkProps) => ( - + {children} - + ) + +const linkStyle = { + color: "#00695c", + textDecoration: "underline", + wordBreak: "break-all" as const, +} From b9cdd92dcc50d5d74524a40cbe821a9bc1dbaa4a Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:09:51 +0000 Subject: [PATCH 85/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 03f7020f..f4f3aa1e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* 💄 Refactor email templates and components for improved styling. PR [#2451](https://github.com/fastapi/full-stack-fastapi-template/pull/2451) by [@alejsdev](https://github.com/alejsdev). * ♻️ Replace Mailcatcher with Mailpit for local email testing. PR [#2436](https://github.com/fastapi/full-stack-fastapi-template/pull/2436) by [@alejsdev](https://github.com/alejsdev). ### Docs From 9a5c827ba6e66ae989950ca520fcdc3da35ec240 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:25:03 +0200 Subject: [PATCH 86/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20README.md=20with?= =?UTF-8?q?=20Mailpit=20and=20React=20Email=20(#2452)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++++++ img/mailpit.png | Bin 0 -> 170567 bytes img/react-email.png | Bin 0 -> 99256 bytes 3 files changed, 9 insertions(+) create mode 100644 img/mailpit.png create mode 100644 img/react-email.png diff --git a/README.md b/README.md index a3e20f4d..ff783583 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - 🔒 Secure password hashing by default. - 🔑 JWT (JSON Web Token) authentication. - 📫 Email-based password recovery. +- ✉️ [React Email](https://react.email) for email templates. - 📬 [Mailpit](https://mailpit.axllent.org) for local email testing during development. - ✅ Tests with [Pytest](https://pytest.org). - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. @@ -42,6 +43,14 @@ ![Dark mode dashboard screenshot](img/dashboard-dark.png) +### React Email Templates + +![Email templates screenshot](img/react-email.png) + +### Mailpit - Local Email Testing + +![Mailpit screenshot](img/mailpit.png) + ### Interactive API Documentation ![API docs](img/docs.png) diff --git a/img/mailpit.png b/img/mailpit.png new file mode 100644 index 0000000000000000000000000000000000000000..35177c6204ff9824316efd5f7a5f264867360d57 GIT binary patch literal 170567 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ν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 literal 0 HcmV?d00001 diff --git a/img/react-email.png b/img/react-email.png new file mode 100644 index 0000000000000000000000000000000000000000..1aa7c200b845e6d1daa811e604d3dc4ea3ec52a3 GIT binary patch literal 99256 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 -@~TTFPf$3_`$Ndag1!9(t(vf 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 literal 0 HcmV?d00001 From 6b5b88676033c8be7352a826fefb59071dd19cfd Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:25:06 +0000 Subject: [PATCH 87/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index f4f3aa1e..a266206f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,6 +14,7 @@ ### Docs +* 📝 Update README.md with Mailpit and React Email. PR [#2452](https://github.com/fastapi/full-stack-fastapi-template/pull/2452) by [@alejsdev](https://github.com/alejsdev). * 📝 Simplify project documentation. PR [#2442](https://github.com/fastapi/full-stack-fastapi-template/pull/2442) by [@tiangolo](https://github.com/tiangolo). ## 0.12.0 (2026-08-12) From 7c11b644a3e6d30ab4dcc27927b7c8fdfb37da1b Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:41:27 +0200 Subject: [PATCH 88/91] =?UTF-8?q?=E2=9C=85=20Simplify=20password=20reset?= =?UTF-8?q?=20E2E=20tests=20(#2453)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/tests/reset-password.spec.ts | 31 +++++++-------- frontend/tests/utils/mailpit.ts | 55 ++++++++++----------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/frontend/tests/reset-password.spec.ts b/frontend/tests/reset-password.spec.ts index fa7be461..0cf5417c 100644 --- a/frontend/tests/reset-password.spec.ts +++ b/frontend/tests/reset-password.spec.ts @@ -1,10 +1,12 @@ import { expect, test } from "@playwright/test" -import { emailUrl, findLastEmail } from "./utils/mailpit" +import { waitForEmailHtml } from "./utils/mailpit" import { randomEmail, randomPassword } from "./utils/random" import { logInUser, signUpNewUser } from "./utils/user" test.use({ storageState: { cookies: [], origins: [] } }) +const resetPath = "/reset-password?token=" + test("Password Recovery title is visible", async ({ page }) => { await page.goto("/recover-password") @@ -44,21 +46,17 @@ test("User can reset password successfully using the link", async ({ await page.getByRole("button", { name: "Continue" }).click() - const emailData = await findLastEmail({ + const emailHtml = await waitForEmailHtml({ request, query: `to:${email}`, - timeout: 5000, }) - await page.goto(emailUrl(emailData)) - - const selector = 'a[href*="/reset-password?token="]' - - const url = await page.getAttribute(selector, "href") - const resetUrl = new URL(url!) + expect(emailHtml).toContain(resetPath) + const resetUrl = emailHtml.match(/\/reset-password\?token=[^"]+/)?.[0] + expect(resetUrl).toBeDefined() // Set the new password and confirm it - await page.goto(`${resetUrl.pathname}${resetUrl.search}`) + await page.goto(resetUrl!) await page.getByTestId("new-password-input").fill(newPassword) await page.getByTestId("confirm-password-input").fill(newPassword) @@ -95,20 +93,17 @@ test("Weak new password validation", async ({ page, request }) => { await page.getByTestId("email-input").fill(email) await page.getByRole("button", { name: "Continue" }).click() - const emailData = await findLastEmail({ + const emailHtml = await waitForEmailHtml({ request, query: `to:${email}`, - timeout: 5000, }) - await page.goto(emailUrl(emailData)) - - const selector = 'a[href*="/reset-password?token="]' - const url = await page.getAttribute(selector, "href") - const resetUrl = new URL(url!) + expect(emailHtml).toContain(resetPath) + const resetUrl = emailHtml.match(/\/reset-password\?token=[^"]+/)?.[0] + expect(resetUrl).toBeDefined() // Set a weak new password - await page.goto(`${resetUrl.pathname}${resetUrl.search}`) + await page.goto(resetUrl!) await page.getByTestId("new-password-input").fill(weakPassword) await page.getByTestId("confirm-password-input").fill(weakPassword) await page.getByRole("button", { name: "Reset Password" }).click() diff --git a/frontend/tests/utils/mailpit.ts b/frontend/tests/utils/mailpit.ts index 92c02e28..6fe8ae76 100644 --- a/frontend/tests/utils/mailpit.ts +++ b/frontend/tests/utils/mailpit.ts @@ -1,36 +1,10 @@ import type { APIRequestContext } from "@playwright/test" -type Address = { - Name: string - Address: string -} - -type Email = { +type EmailSummary = { ID: string - To: Address[] - Subject: string } -async function findEmail({ - request, - query, -}: { - request: APIRequestContext - query: string -}) { - const response = await request.get( - `${process.env.MAILPIT_HOST}/api/v1/search`, - { - params: { query, limit: 1 }, - }, - ) - - const { messages }: { messages: Email[] } = await response.json() - - return messages[0] ?? null -} - -export async function findLastEmail({ +export async function waitForEmailHtml({ request, query, timeout = 5000, @@ -42,10 +16,27 @@ export async function findLastEmail({ const deadline = Date.now() + timeout while (Date.now() < deadline) { - const email = await findEmail({ request, query }) + const response = await request.get( + `${process.env.MAILPIT_HOST}/api/v1/search`, + { + params: { query, limit: 1 }, + }, + ) + const { messages }: { messages: EmailSummary[] } = await response.json() + const email = messages[0] if (email) { - return email + const htmlResponse = await request.get( + `${process.env.MAILPIT_HOST}/view/${email.ID}.html`, + ) + + if (!htmlResponse.ok()) { + throw new Error( + `Could not get the HTML for email "${email.ID}": ${htmlResponse.status()}`, + ) + } + + return htmlResponse.text() } await new Promise((resolve) => setTimeout(resolve, 100)) @@ -53,7 +44,3 @@ export async function findLastEmail({ throw new Error(`Timeout while trying to get the latest email for "${query}"`) } - -export function emailUrl(email: Email) { - return `${process.env.MAILPIT_HOST}/view/${email.ID}.html` -} From 68adb40d37425f6f8668ec7e5a054500d045e43e Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:41:30 +0000 Subject: [PATCH 89/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index a266206f..9b50b280 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ✅ Simplify password reset E2E tests. PR [#2453](https://github.com/fastapi/full-stack-fastapi-template/pull/2453) by [@alejsdev](https://github.com/alejsdev). * 💄 Refactor email templates and components for improved styling. PR [#2451](https://github.com/fastapi/full-stack-fastapi-template/pull/2451) by [@alejsdev](https://github.com/alejsdev). * ♻️ Replace Mailcatcher with Mailpit for local email testing. PR [#2436](https://github.com/fastapi/full-stack-fastapi-template/pull/2436) by [@alejsdev](https://github.com/alejsdev). From 4d1a75de20291f5148f717dc301d3c1bc4826c36 Mon Sep 17 00:00:00 2001 From: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:07:31 +0200 Subject: [PATCH 90/91] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20setup-uv=20ac?= =?UTF-8?q?tion=20to=2010.0.1=20(#2458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/bump-pre-commit-hooks.yml | 6 ++---- .github/workflows/create-draft-release.yml | 6 ++---- .github/workflows/deploy.yml | 4 ++-- .github/workflows/playwright.yml | 6 ++---- .github/workflows/pre-commit.yml | 6 ++---- .github/workflows/prepare-release.yml | 6 ++---- .github/workflows/smokeshow.yml | 6 ++---- .github/workflows/test-backend.yml | 6 ++---- 8 files changed, 16 insertions(+), 30 deletions(-) diff --git a/.github/workflows/bump-pre-commit-hooks.yml b/.github/workflows/bump-pre-commit-hooks.yml index c549b207..84bb0a73 100644 --- a/.github/workflows/bump-pre-commit-hooks.yml +++ b/.github/workflows/bump-pre-commit-hooks.yml @@ -28,11 +28,9 @@ jobs: with: python-version-file: ".python-version" - name: Setup uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. - # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 - version: "0.11.18" + version: "latest-known" cache-dependency-glob: | pyproject.toml uv.lock diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml index da68af95..a851e7c5 100644 --- a/.github/workflows/create-draft-release.yml +++ b/.github/workflows/create-draft-release.yml @@ -29,11 +29,9 @@ jobs: with: python-version-file: ".python-version" - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. - # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 - version: "0.11.18" + version: "latest-known" - name: Extract release details id: release-details run: | diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 82e7efd2..d896b03a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,9 +35,9 @@ jobs: # Override frontend/.env so the deployed frontend uses the same origin as the API VITE_API_URL: "" - name: Set up uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - version: "0.11.18" + version: "latest-known" - name: Prepare database run: uv run bash scripts/prestart.sh working-directory: backend diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 75f6b591..cc5f7ade 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -66,11 +66,9 @@ jobs: with: limit-access-to-actor: true - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. - # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 - version: "0.11.18" + version: "latest-known" - run: uv sync working-directory: backend - run: bun ci diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index a1ea6d2c..21955494 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -46,11 +46,9 @@ jobs: with: python-version-file: .python-version - name: Setup uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. - # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 - version: "0.11.18" + version: "latest-known" cache-dependency-glob: | requirements**.txt pyproject.toml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index fa0e033b..24dbbab8 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -38,11 +38,9 @@ jobs: with: python-version-file: ".python-version" - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. - # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 - version: "0.11.18" + version: "latest-known" - name: Prepare release env: BUMP: ${{ inputs.bump }} diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 1483e5b3..67e8b75f 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -24,11 +24,9 @@ jobs: with: python-version-file: .python-version - name: Setup uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. - # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 - version: "0.11.18" + version: "latest-known" cache-dependency-glob: | pyproject.toml uv.lock diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 92c5fc98..477a6b44 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -22,11 +22,9 @@ jobs: with: python-version-file: .python-version - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. - # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 - version: "0.11.18" + version: "latest-known" - run: docker compose down -v --remove-orphans - run: docker compose up -d db mailpit - name: Migrate DB From 8063fe54f17d19f01720e055103af9cad3d8f55d Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:07:34 +0000 Subject: [PATCH 91/91] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 9b50b280..8d855725 100644 --- a/release-notes.md +++ b/release-notes.md @@ -18,6 +18,10 @@ * 📝 Update README.md with Mailpit and React Email. PR [#2452](https://github.com/fastapi/full-stack-fastapi-template/pull/2452) by [@alejsdev](https://github.com/alejsdev). * 📝 Simplify project documentation. PR [#2442](https://github.com/fastapi/full-stack-fastapi-template/pull/2442) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ⬆️ Bump setup-uv action to 10.0.1. PR [#2458](https://github.com/fastapi/full-stack-fastapi-template/pull/2458) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.12.0 (2026-08-12) ### Features