Merge branch 'feat_AEB_25_create_auth_service' into 'main'
feat / AEB-25 Create auth service See merge request wedeving/aerbim-www!2
This commit is contained in:
@@ -6,12 +6,16 @@ name = "pypi"
|
|||||||
[packages]
|
[packages]
|
||||||
django = "*"
|
django = "*"
|
||||||
djangorestframework = "*"
|
djangorestframework = "*"
|
||||||
pycodestyle = "*"
|
|
||||||
django-cors-headers = "*"
|
django-cors-headers = "*"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
dotenv = "*"
|
dotenv = "*"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-django = "*"
|
pytest-django = "*"
|
||||||
|
djangorestframework-simplejwt = "*"
|
||||||
|
pycodestyle = "*"
|
||||||
|
requests = "*"
|
||||||
|
pyjwt = "*"
|
||||||
|
drf-spectacular = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|||||||
423
backend/Pipfile.lock
generated
423
backend/Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "e150d0e5553cb65551da7428eb7dd6810099cf0188118c2031d785f562315b37"
|
"sha256": "c8a08d8710d5b37141e66db971439bf41996f2ea1330f2d5716f8be2a841a796"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -24,6 +24,107 @@
|
|||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==3.9.1"
|
"version": "==3.9.1"
|
||||||
},
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3",
|
||||||
|
"sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==25.3.0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407",
|
||||||
|
"sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2025.8.3"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91",
|
||||||
|
"sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0",
|
||||||
|
"sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154",
|
||||||
|
"sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601",
|
||||||
|
"sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884",
|
||||||
|
"sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07",
|
||||||
|
"sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c",
|
||||||
|
"sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64",
|
||||||
|
"sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe",
|
||||||
|
"sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f",
|
||||||
|
"sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432",
|
||||||
|
"sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc",
|
||||||
|
"sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa",
|
||||||
|
"sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9",
|
||||||
|
"sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae",
|
||||||
|
"sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19",
|
||||||
|
"sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d",
|
||||||
|
"sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e",
|
||||||
|
"sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4",
|
||||||
|
"sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7",
|
||||||
|
"sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312",
|
||||||
|
"sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92",
|
||||||
|
"sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31",
|
||||||
|
"sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c",
|
||||||
|
"sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f",
|
||||||
|
"sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99",
|
||||||
|
"sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b",
|
||||||
|
"sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15",
|
||||||
|
"sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392",
|
||||||
|
"sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f",
|
||||||
|
"sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8",
|
||||||
|
"sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491",
|
||||||
|
"sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0",
|
||||||
|
"sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc",
|
||||||
|
"sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0",
|
||||||
|
"sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f",
|
||||||
|
"sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a",
|
||||||
|
"sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40",
|
||||||
|
"sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927",
|
||||||
|
"sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849",
|
||||||
|
"sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce",
|
||||||
|
"sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14",
|
||||||
|
"sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05",
|
||||||
|
"sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c",
|
||||||
|
"sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c",
|
||||||
|
"sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a",
|
||||||
|
"sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc",
|
||||||
|
"sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34",
|
||||||
|
"sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9",
|
||||||
|
"sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096",
|
||||||
|
"sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14",
|
||||||
|
"sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30",
|
||||||
|
"sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b",
|
||||||
|
"sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b",
|
||||||
|
"sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942",
|
||||||
|
"sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db",
|
||||||
|
"sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5",
|
||||||
|
"sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b",
|
||||||
|
"sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce",
|
||||||
|
"sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669",
|
||||||
|
"sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0",
|
||||||
|
"sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018",
|
||||||
|
"sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93",
|
||||||
|
"sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe",
|
||||||
|
"sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049",
|
||||||
|
"sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a",
|
||||||
|
"sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef",
|
||||||
|
"sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2",
|
||||||
|
"sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca",
|
||||||
|
"sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16",
|
||||||
|
"sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f",
|
||||||
|
"sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb",
|
||||||
|
"sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1",
|
||||||
|
"sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557",
|
||||||
|
"sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37",
|
||||||
|
"sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7",
|
||||||
|
"sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72",
|
||||||
|
"sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c",
|
||||||
|
"sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.4.3"
|
||||||
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae",
|
"sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae",
|
||||||
@@ -51,6 +152,15 @@
|
|||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==3.16.1"
|
"version": "==3.16.1"
|
||||||
},
|
},
|
||||||
|
"djangorestframework-simplejwt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469",
|
||||||
|
"sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==5.5.1"
|
||||||
|
},
|
||||||
"dotenv": {
|
"dotenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
|
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
|
||||||
@@ -58,6 +168,31 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.9.9"
|
"version": "==0.9.9"
|
||||||
},
|
},
|
||||||
|
"drf-spectacular": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c778a47a40ab2f5078a7c42e82baba07397bb35b074ae4680721b2805943061",
|
||||||
|
"sha256:856e7edf1056e49a4245e87a61e8da4baff46c83dbc25be1da2df77f354c7cb4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==0.28.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
|
||||||
|
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.10"
|
||||||
|
},
|
||||||
|
"inflection": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||||
|
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==0.5.1"
|
||||||
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7",
|
"sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7",
|
||||||
@@ -66,6 +201,22 @@
|
|||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.1.0"
|
"version": "==2.1.0"
|
||||||
},
|
},
|
||||||
|
"jsonschema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63",
|
||||||
|
"sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==4.25.1"
|
||||||
|
},
|
||||||
|
"jsonschema-specifications": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af",
|
||||||
|
"sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2025.4.1"
|
||||||
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
|
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
|
||||||
@@ -174,6 +325,15 @@
|
|||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.19.2"
|
"version": "==2.19.2"
|
||||||
},
|
},
|
||||||
|
"pyjwt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953",
|
||||||
|
"sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.10.1"
|
||||||
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7",
|
"sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7",
|
||||||
@@ -200,6 +360,243 @@
|
|||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==1.1.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff",
|
||||||
|
"sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48",
|
||||||
|
"sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086",
|
||||||
|
"sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e",
|
||||||
|
"sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133",
|
||||||
|
"sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5",
|
||||||
|
"sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484",
|
||||||
|
"sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee",
|
||||||
|
"sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5",
|
||||||
|
"sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68",
|
||||||
|
"sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a",
|
||||||
|
"sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf",
|
||||||
|
"sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99",
|
||||||
|
"sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8",
|
||||||
|
"sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85",
|
||||||
|
"sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19",
|
||||||
|
"sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc",
|
||||||
|
"sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a",
|
||||||
|
"sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1",
|
||||||
|
"sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317",
|
||||||
|
"sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c",
|
||||||
|
"sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631",
|
||||||
|
"sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d",
|
||||||
|
"sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652",
|
||||||
|
"sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5",
|
||||||
|
"sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e",
|
||||||
|
"sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b",
|
||||||
|
"sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8",
|
||||||
|
"sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476",
|
||||||
|
"sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706",
|
||||||
|
"sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563",
|
||||||
|
"sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237",
|
||||||
|
"sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b",
|
||||||
|
"sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083",
|
||||||
|
"sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180",
|
||||||
|
"sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425",
|
||||||
|
"sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e",
|
||||||
|
"sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f",
|
||||||
|
"sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725",
|
||||||
|
"sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183",
|
||||||
|
"sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab",
|
||||||
|
"sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774",
|
||||||
|
"sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725",
|
||||||
|
"sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e",
|
||||||
|
"sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5",
|
||||||
|
"sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d",
|
||||||
|
"sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290",
|
||||||
|
"sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44",
|
||||||
|
"sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed",
|
||||||
|
"sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4",
|
||||||
|
"sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba",
|
||||||
|
"sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12",
|
||||||
|
"sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==6.0.2"
|
||||||
|
},
|
||||||
|
"referencing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa",
|
||||||
|
"sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==0.36.2"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6",
|
||||||
|
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.32.5"
|
||||||
|
},
|
||||||
|
"rpds-py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400",
|
||||||
|
"sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1",
|
||||||
|
"sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e",
|
||||||
|
"sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f",
|
||||||
|
"sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60",
|
||||||
|
"sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059",
|
||||||
|
"sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2",
|
||||||
|
"sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff",
|
||||||
|
"sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef",
|
||||||
|
"sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd",
|
||||||
|
"sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf",
|
||||||
|
"sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d",
|
||||||
|
"sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e",
|
||||||
|
"sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52",
|
||||||
|
"sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8",
|
||||||
|
"sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d",
|
||||||
|
"sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc",
|
||||||
|
"sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5",
|
||||||
|
"sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8",
|
||||||
|
"sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf",
|
||||||
|
"sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c",
|
||||||
|
"sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418",
|
||||||
|
"sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746",
|
||||||
|
"sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905",
|
||||||
|
"sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688",
|
||||||
|
"sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39",
|
||||||
|
"sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb",
|
||||||
|
"sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502",
|
||||||
|
"sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66",
|
||||||
|
"sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b",
|
||||||
|
"sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc",
|
||||||
|
"sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675",
|
||||||
|
"sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013",
|
||||||
|
"sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1",
|
||||||
|
"sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1",
|
||||||
|
"sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a",
|
||||||
|
"sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734",
|
||||||
|
"sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5",
|
||||||
|
"sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e",
|
||||||
|
"sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92",
|
||||||
|
"sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c",
|
||||||
|
"sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195",
|
||||||
|
"sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786",
|
||||||
|
"sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274",
|
||||||
|
"sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3",
|
||||||
|
"sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859",
|
||||||
|
"sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a",
|
||||||
|
"sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125",
|
||||||
|
"sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71",
|
||||||
|
"sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83",
|
||||||
|
"sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3",
|
||||||
|
"sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5",
|
||||||
|
"sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817",
|
||||||
|
"sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48",
|
||||||
|
"sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772",
|
||||||
|
"sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2",
|
||||||
|
"sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948",
|
||||||
|
"sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef",
|
||||||
|
"sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde",
|
||||||
|
"sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9",
|
||||||
|
"sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802",
|
||||||
|
"sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3",
|
||||||
|
"sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab",
|
||||||
|
"sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be",
|
||||||
|
"sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6",
|
||||||
|
"sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8",
|
||||||
|
"sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad",
|
||||||
|
"sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf",
|
||||||
|
"sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec",
|
||||||
|
"sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4",
|
||||||
|
"sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1",
|
||||||
|
"sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a",
|
||||||
|
"sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8",
|
||||||
|
"sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39",
|
||||||
|
"sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4",
|
||||||
|
"sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab",
|
||||||
|
"sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808",
|
||||||
|
"sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5",
|
||||||
|
"sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10",
|
||||||
|
"sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797",
|
||||||
|
"sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3",
|
||||||
|
"sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61",
|
||||||
|
"sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228",
|
||||||
|
"sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4",
|
||||||
|
"sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf",
|
||||||
|
"sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881",
|
||||||
|
"sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002",
|
||||||
|
"sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52",
|
||||||
|
"sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9",
|
||||||
|
"sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1",
|
||||||
|
"sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f",
|
||||||
|
"sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998",
|
||||||
|
"sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485",
|
||||||
|
"sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456",
|
||||||
|
"sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd",
|
||||||
|
"sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e",
|
||||||
|
"sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475",
|
||||||
|
"sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e",
|
||||||
|
"sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c",
|
||||||
|
"sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334",
|
||||||
|
"sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90",
|
||||||
|
"sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2",
|
||||||
|
"sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657",
|
||||||
|
"sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15",
|
||||||
|
"sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b",
|
||||||
|
"sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33",
|
||||||
|
"sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2",
|
||||||
|
"sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8",
|
||||||
|
"sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881",
|
||||||
|
"sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136",
|
||||||
|
"sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212",
|
||||||
|
"sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc",
|
||||||
|
"sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0",
|
||||||
|
"sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e",
|
||||||
|
"sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819",
|
||||||
|
"sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527",
|
||||||
|
"sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed",
|
||||||
|
"sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df",
|
||||||
|
"sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb",
|
||||||
|
"sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a",
|
||||||
|
"sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a",
|
||||||
|
"sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21",
|
||||||
|
"sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf",
|
||||||
|
"sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8",
|
||||||
|
"sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594",
|
||||||
|
"sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a",
|
||||||
|
"sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e",
|
||||||
|
"sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7",
|
||||||
|
"sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8",
|
||||||
|
"sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6",
|
||||||
|
"sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3",
|
||||||
|
"sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec",
|
||||||
|
"sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3",
|
||||||
|
"sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723",
|
||||||
|
"sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b",
|
||||||
|
"sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb",
|
||||||
|
"sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081",
|
||||||
|
"sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7",
|
||||||
|
"sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d",
|
||||||
|
"sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9",
|
||||||
|
"sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9",
|
||||||
|
"sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4",
|
||||||
|
"sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444",
|
||||||
|
"sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a",
|
||||||
|
"sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0",
|
||||||
|
"sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b",
|
||||||
|
"sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83",
|
||||||
|
"sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3",
|
||||||
|
"sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636",
|
||||||
|
"sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc",
|
||||||
|
"sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2",
|
||||||
|
"sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a",
|
||||||
|
"sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb",
|
||||||
|
"sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec",
|
||||||
|
"sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==0.27.1"
|
||||||
|
},
|
||||||
"sqlparse": {
|
"sqlparse": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272",
|
"sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272",
|
||||||
@@ -207,6 +604,30 @@
|
|||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==0.5.3"
|
"version": "==0.5.3"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
|
||||||
|
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==4.15.0"
|
||||||
|
},
|
||||||
|
"uritemplate": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e",
|
||||||
|
"sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==4.2.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760",
|
||||||
|
"sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|||||||
0
backend/api/auth/__init__.py
Normal file
0
backend/api/auth/__init__.py
Normal file
114
backend/api/auth/serializers.py
Normal file
114
backend/api/auth/serializers.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
from typing import Any, Optional
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.conf import settings
|
||||||
|
from api.types import User
|
||||||
|
|
||||||
|
class UserResponseSerializer(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
email = serializers.EmailField(read_only=True)
|
||||||
|
account_type = serializers.CharField(
|
||||||
|
source='userprofile.account_type',
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
name = serializers.CharField(source='first_name', read_only=True)
|
||||||
|
surname = serializers.CharField(source='last_name', read_only=True)
|
||||||
|
imageURL = serializers.SerializerMethodField()
|
||||||
|
uuid = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ref_name = "UserResponse" # для OpenAPI
|
||||||
|
|
||||||
|
def get_uuid(self, obj: User) -> Optional[str]:
|
||||||
|
"""Получает короткий UUID (первые 6 символов) из профиля пользователя"""
|
||||||
|
return obj.userprofile.short_uuid if hasattr(obj, 'userprofile') else None
|
||||||
|
|
||||||
|
def get_imageURL(self, obj: User) -> Optional[str]:
|
||||||
|
"""Получает полный URL для изображения профиля пользователя"""
|
||||||
|
try:
|
||||||
|
if not hasattr(obj, 'userprofile') or not obj.userprofile.imageURL:
|
||||||
|
return None
|
||||||
|
|
||||||
|
relative_url = obj.userprofile.imageURL.lstrip('/')
|
||||||
|
base_url = settings.BASE_URL.rstrip('/')
|
||||||
|
return f"{base_url}/{relative_url}"
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_representation(self, instance: User) -> dict[str, Any]:
|
||||||
|
"""Переопределяется для добавления проверки типа для вывода"""
|
||||||
|
data = super().to_representation(instance)
|
||||||
|
return {
|
||||||
|
'id': data['id'], # int
|
||||||
|
'email': data['email'], # str
|
||||||
|
'account_type': data['account_type'], # AccountTypeLiteral
|
||||||
|
'name': data['name'], # str
|
||||||
|
'surname': data['surname'], # str
|
||||||
|
'imageURL': data['imageURL'], # Optional[str]
|
||||||
|
'uuid': data['uuid'], # Optional[str]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequestSerializer(serializers.Serializer):
|
||||||
|
"""Сериализатор для запроса авторизации"""
|
||||||
|
login = serializers.CharField(
|
||||||
|
help_text="Логин пользователя",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
password = serializers.CharField(
|
||||||
|
help_text="Пароль пользователя",
|
||||||
|
write_only=True,
|
||||||
|
required=True,
|
||||||
|
style={'input_type': 'password'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginResponseSerializer(serializers.Serializer):
|
||||||
|
"""Сериализатор для ответа при успешной авторизации"""
|
||||||
|
message = serializers.CharField(
|
||||||
|
help_text="Сообщение о успешной авторизации",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
access = serializers.CharField(
|
||||||
|
help_text="JWT access token для авторизации запросов",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
refresh = serializers.CharField(
|
||||||
|
help_text="JWT refresh token для обновления access token",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
user = UserResponseSerializer(
|
||||||
|
help_text="Данные авторизованного пользователя",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutResponseSerializer(serializers.Serializer):
|
||||||
|
"""Сериализатор для ответа при выходе из системы"""
|
||||||
|
message = serializers.CharField(
|
||||||
|
help_text="Сообщение о успешном выходе",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenRequestSerializer(serializers.Serializer):
|
||||||
|
"""Сериализатор для запроса обновления токена"""
|
||||||
|
refresh = serializers.CharField(
|
||||||
|
help_text="Refresh token для обновления",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenResponseSerializer(serializers.Serializer):
|
||||||
|
"""Сериализатор для ответа с обновленными токенами"""
|
||||||
|
access = serializers.CharField(
|
||||||
|
help_text="Новый JWT access token",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
refresh = serializers.CharField(
|
||||||
|
help_text="Новый JWT refresh token",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
expires_at = serializers.FloatField(
|
||||||
|
help_text="Timestamp времени истечения access token",
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
29
backend/api/auth/urls.py
Normal file
29
backend/api/auth/urls.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from .views import LoginViewSet, LogoutView, RefreshTokenView
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from drf_spectacular.views import (
|
||||||
|
SpectacularAPIView,
|
||||||
|
SpectacularSwaggerView,
|
||||||
|
SpectacularRedocView,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'', LoginViewSet, basename='auth')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(router.urls)),
|
||||||
|
path('logout/', LogoutView.as_view(), name='auth-logout'),
|
||||||
|
path('refresh/', RefreshTokenView.as_view(), name='token-refresh'),
|
||||||
|
path('schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
|
path(
|
||||||
|
'docs/',
|
||||||
|
SpectacularSwaggerView.as_view(url_name='schema'),
|
||||||
|
name='swagger-ui',
|
||||||
|
),
|
||||||
|
# ReDoc UI - альтернативный вариант отображения доков:
|
||||||
|
path(
|
||||||
|
'redoc/',
|
||||||
|
SpectacularRedocView.as_view(url_name='schema'),
|
||||||
|
name='redoc',
|
||||||
|
),
|
||||||
|
]
|
||||||
259
backend/api/auth/views.py
Normal file
259
backend/api/auth/views.py
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
from rest_framework import status
|
||||||
|
from datetime import datetime
|
||||||
|
import traceback
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from .serializers import (
|
||||||
|
UserResponseSerializer,
|
||||||
|
LoginRequestSerializer,
|
||||||
|
LoginResponseSerializer,
|
||||||
|
LogoutResponseSerializer,
|
||||||
|
RefreshTokenRequestSerializer,
|
||||||
|
RefreshTokenResponseSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
from api.utils.cookies import AuthBaseViewSet
|
||||||
|
from api.types import User
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=['Логин'])
|
||||||
|
class LoginViewSet(AuthBaseViewSet):
|
||||||
|
"""ViewSet для авторизации пользователей"""
|
||||||
|
serializer_class = LoginRequestSerializer
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Авторизация пользователя",
|
||||||
|
description="Эндпоинт для авторизации пользователя по логину и паролю",
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
response=LoginResponseSerializer,
|
||||||
|
description="Успешная авторизация",
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Успешный ответ',
|
||||||
|
value={
|
||||||
|
"message": "Успешная авторизация",
|
||||||
|
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"email": "user@example.com",
|
||||||
|
"account_type": "engieneer",
|
||||||
|
"name": "Иван",
|
||||||
|
"surname": "Иванов",
|
||||||
|
"imageURL": "https://example.com/avatar.jpg",
|
||||||
|
"uuid": "abc123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
400: OpenApiResponse(
|
||||||
|
description="Неверные параметры запроса",
|
||||||
|
response=OpenApiTypes.OBJECT,
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Отсутствуют обязательные поля',
|
||||||
|
value={"error": "Логин и пароль обязательны"}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
403: OpenApiResponse(
|
||||||
|
description="Неверный пароль",
|
||||||
|
response=OpenApiTypes.OBJECT,
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Неверный пароль',
|
||||||
|
value={"error": "Неверный пароль"}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
404: OpenApiResponse(
|
||||||
|
description="Пользователь не найден",
|
||||||
|
response=OpenApiTypes.OBJECT,
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Пользователь не найден',
|
||||||
|
value={"error": "Пользователь не найден"}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
500: OpenApiResponse(
|
||||||
|
description="Внутренняя ошибка сервера",
|
||||||
|
response=OpenApiTypes.OBJECT,
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Ошибка сервера',
|
||||||
|
value={"error": "Ошибка авторизации"}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=['post'], url_path="login")
|
||||||
|
def login_client(self, request):
|
||||||
|
try:
|
||||||
|
login = request.data.get("login")
|
||||||
|
password = request.data.get("password")
|
||||||
|
|
||||||
|
if not login or not password:
|
||||||
|
return Response(
|
||||||
|
{"error": "Логин и пароль обязательны"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(login=login)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Пользователь не найден"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
if not user.check_password(password):
|
||||||
|
return Response(
|
||||||
|
{"error": "Неверный пароль"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
|
user_data = UserResponseSerializer(user).data
|
||||||
|
|
||||||
|
response = Response({
|
||||||
|
"message": "Успешная авторизация",
|
||||||
|
"access": str(refresh.access_token),
|
||||||
|
"refresh": str(refresh),
|
||||||
|
"user": user_data
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# сеттим куки
|
||||||
|
return self._set_auth_cookies(response, refresh)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{"error": "Ошибка авторизации"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(tags=['Логин'])
|
||||||
|
class RefreshTokenView(APIView):
|
||||||
|
"""View для обновления JWT токенов"""
|
||||||
|
serializer_class = RefreshTokenRequestSerializer
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Обновление токенов",
|
||||||
|
description="Эндпоинт для обновления JWT токенов. Принимает refresh token и возвращает новую пару токенов.",
|
||||||
|
request=RefreshTokenRequestSerializer,
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
response=RefreshTokenResponseSerializer,
|
||||||
|
description="Токены успешно обновлены",
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Успешное обновление',
|
||||||
|
value={
|
||||||
|
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"expires_at": 1679831642.0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
400: OpenApiResponse(
|
||||||
|
description="Ошибка обновления токена",
|
||||||
|
response=OpenApiTypes.OBJECT,
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Отсутствует refresh token',
|
||||||
|
value={"error": "Refresh token is required"}
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
'Невалидный refresh token',
|
||||||
|
value={"error": "Invalid refresh token: Token is invalid or expired"}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
refresh_token = request.data.get('refresh')
|
||||||
|
|
||||||
|
if not refresh_token:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Требуется refresh token'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = RefreshToken(refresh_token)
|
||||||
|
|
||||||
|
# точное время истечения токена
|
||||||
|
expires_at = datetime.now() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME']
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'access': str(token.access_token),
|
||||||
|
'refresh': str(token),
|
||||||
|
'expires_at': datetime.timestamp(expires_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# логируем ошибки
|
||||||
|
|
||||||
|
print(f"Token refresh error: {str(e)}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{'error': f'Невалидный refresh token: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Ошибка обновления токена: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(tags=['Логаут'])
|
||||||
|
class LogoutView(APIView):
|
||||||
|
"""View для выхода из системы"""
|
||||||
|
serializer_class = LogoutResponseSerializer
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Выход из системы",
|
||||||
|
description="Эндпоинт для выхода из системы. Очищает JWT токены и сессионные куки.",
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
response=LogoutResponseSerializer,
|
||||||
|
description="Успешный выход из системы",
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Успешный выход',
|
||||||
|
value={"message": "Успешный выход из системы"}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
"""Выход из системы с очисткой всех токенов и куки"""
|
||||||
|
response = Response(
|
||||||
|
{'message': 'Успешный выход из системы'},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
response.delete_cookie('access_token')
|
||||||
|
response.delete_cookie('refresh_token')
|
||||||
|
response.delete_cookie('sessionid')
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -1,3 +1,56 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.fields.related import OneToOneField
|
||||||
|
from typing import Optional
|
||||||
|
import uuid
|
||||||
|
|
||||||
# Create your models here.
|
from sitemanagement.constants.account_types import account_types, AccountType, AccountTypeLiteral
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
"""
|
||||||
|
Профиль пользователя с дополнительной информацией
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_account_type_display(self) -> str:
|
||||||
|
"""Автоматически добавляется Django для полей с choices"""
|
||||||
|
...
|
||||||
|
|
||||||
|
user: OneToOneField[User] = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='userprofile'
|
||||||
|
)
|
||||||
|
uuid: models.UUIDField = models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
unique=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
account_type: models.CharField = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="Тип аккаунта",
|
||||||
|
choices=account_types,
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
|
imageURL: models.CharField = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="URL изображения профиля"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Профиль пользователя"
|
||||||
|
verbose_name_plural = "Профили пользователей"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.user.first_name} ({self.get_account_type_display()})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_uuid(self) -> Optional[str]:
|
||||||
|
"""Возвращает первые 6 символов UUID или None, если UUID не установлен"""
|
||||||
|
return str(self.uuid)[:6] if self.uuid else None
|
||||||
|
|
||||||
|
def get_account_type(self) -> AccountTypeLiteral:
|
||||||
|
"""Возвращает тип аккаунта пользователя"""
|
||||||
|
return AccountType(self.account_type).value
|
||||||
9
backend/api/types.py
Normal file
9
backend/api/types.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from django.contrib.auth.models import User as DjangoUser
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .models import UserProfile
|
||||||
|
|
||||||
|
class User(DjangoUser):
|
||||||
|
"""Тип для Django User с кастомной моделью UserProfile"""
|
||||||
|
userprofile: 'UserProfile'
|
||||||
5
backend/api/urls.py
Normal file
5
backend/api/urls.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('v1/auth/', include('api.auth.urls'))
|
||||||
|
]
|
||||||
65
backend/api/utils/cookies.py
Normal file
65
backend/api/utils/cookies.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from datetime import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
class AuthBaseViewSet(ViewSet):
|
||||||
|
"""Базовый класс для аутентификации с общими методами"""
|
||||||
|
|
||||||
|
def _set_auth_cookies(self, response, refresh):
|
||||||
|
"""Устанавливает куки для токенов аутентификации"""
|
||||||
|
response.set_cookie(
|
||||||
|
'access_token',
|
||||||
|
str(refresh.access_token),
|
||||||
|
httponly=True,
|
||||||
|
secure=True,
|
||||||
|
samesite='Lax',
|
||||||
|
max_age=300
|
||||||
|
)
|
||||||
|
response.set_cookie(
|
||||||
|
'refresh_token',
|
||||||
|
str(refresh),
|
||||||
|
httponly=True,
|
||||||
|
secure=True,
|
||||||
|
samesite='Lax',
|
||||||
|
max_age=86400
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path="refresh")
|
||||||
|
def refresh_token(self, request):
|
||||||
|
try:
|
||||||
|
refresh_token = request.data.get('refresh')
|
||||||
|
|
||||||
|
if not refresh_token:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Refresh token is required'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = RefreshToken(refresh_token)
|
||||||
|
response_data = {
|
||||||
|
'access': str(token.access_token),
|
||||||
|
'refresh': str(token),
|
||||||
|
'expires_at': datetime.timestamp(
|
||||||
|
datetime.now() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Invalid refresh token: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Token refresh failed: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
"""
|
|
||||||
ASGI config for base project.
|
|
||||||
|
|
||||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(dotenv_path=BASE_DIR / './.env')
|
load_dotenv(dotenv_path=BASE_DIR / './.env')
|
||||||
@@ -33,6 +34,27 @@ CORS_ALLOW_HEADERS = [
|
|||||||
'cookie'
|
'cookie'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
],
|
||||||
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
||||||
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||||
|
'ROTATE_REFRESH_TOKENS': True,
|
||||||
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
|
'UPDATE_LAST_LOGIN': True,
|
||||||
|
'ALGORITHM': 'HS256',
|
||||||
|
'SIGNING_KEY': SECRET_KEY,
|
||||||
|
'VERIFYING_KEY': None,
|
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||||
|
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
||||||
|
}
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
@@ -45,9 +67,36 @@ INSTALLED_APPS = [
|
|||||||
'api.apps.ApiConfig',
|
'api.apps.ApiConfig',
|
||||||
'sitemanagement.apps.SitemanagementConfig',
|
'sitemanagement.apps.SitemanagementConfig',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'drf_spectacular',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
#!OpenAPI
|
||||||
|
SPECTACULAR_SETTINGS = {
|
||||||
|
'TITLE': 'AERBIM API',
|
||||||
|
'DESCRIPTION': 'AERBIM - документация по API',
|
||||||
|
'VERSION': '1.0.0',
|
||||||
|
'SERVE_INCLUDE_SCHEMA': False,
|
||||||
|
'SCHEMA_PATH_PREFIX': '/api/v[0-9]',
|
||||||
|
'COMPONENT_SPLIT_REQUEST': False, # не создавать автоматически *Request схемы
|
||||||
|
|
||||||
|
'COMPONENT_NO_READ_ONLY_REQUIRED': True, # не требовать read_only поля
|
||||||
|
'COMPONENT_SPLIT_PATCHES': False, # не создавать отдельные схемы для PATCH
|
||||||
|
|
||||||
|
# настройки безопасности
|
||||||
|
'SECURITY': [{'Bearer': []}],
|
||||||
|
'SWAGGER_UI_SETTINGS': {
|
||||||
|
'persistAuthorization': True,
|
||||||
|
},
|
||||||
|
|
||||||
|
# сортировка тегов и операций
|
||||||
|
'TAGS': [
|
||||||
|
{'name': 'Логаут', 'description': 'Метод для работы с логаутом'},
|
||||||
|
{'name': 'Логин', 'description': 'Методы для работы с логином'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@@ -104,10 +153,11 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'ru-ru'
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'Europe/Minsk'
|
||||||
USE_I18N = True
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
USE_I18N = True
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
"""
|
|
||||||
URL configuration for base project.
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/', include('api.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
"""
|
|
||||||
WSGI config for base project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
asgiref==3.9.1
|
asgiref==3.9.1
|
||||||
|
certifi==2025.8.3
|
||||||
|
charset-normalizer==3.4.3
|
||||||
Django==5.2.5
|
Django==5.2.5
|
||||||
django-cors-headers==4.7.0
|
django-cors-headers==4.7.0
|
||||||
djangorestframework==3.16.1
|
djangorestframework==3.16.1
|
||||||
|
djangorestframework_simplejwt==5.5.1
|
||||||
dotenv==0.9.9
|
dotenv==0.9.9
|
||||||
|
idna==3.10
|
||||||
iniconfig==2.1.0
|
iniconfig==2.1.0
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
pluggy==1.6.0
|
pluggy==1.6.0
|
||||||
psycopg2-binary==2.9.10
|
psycopg2-binary==2.9.10
|
||||||
pycodestyle==2.14.0
|
pycodestyle==2.14.0
|
||||||
Pygments==2.19.2
|
Pygments==2.19.2
|
||||||
|
PyJWT==2.10.1
|
||||||
pytest==8.4.1
|
pytest==8.4.1
|
||||||
pytest-django==4.11.1
|
pytest-django==4.11.1
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
|
requests==2.32.5
|
||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
|
urllib3==2.5.0
|
||||||
|
drf-spectacular==0.27.1
|
||||||
|
|||||||
19
backend/sitemanagement/constants/account_types.py
Normal file
19
backend/sitemanagement/constants/account_types.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from typing import Literal, Tuple, List
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class AccountType(str, Enum):
|
||||||
|
ENGINEER = "engineer"
|
||||||
|
OPERATOR = "operator"
|
||||||
|
ADMIN = "admin"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def choices(cls) -> List[Tuple[str, str]]:
|
||||||
|
return [
|
||||||
|
(cls.ENGINEER.value, "Инженер"),
|
||||||
|
(cls.OPERATOR.value, "Оператор"),
|
||||||
|
(cls.ADMIN.value, "Администратор"),
|
||||||
|
]
|
||||||
|
|
||||||
|
AccountTypeLiteral = Literal["engineer", "operator", "admin"]
|
||||||
|
|
||||||
|
account_types = AccountType.choices()
|
||||||
9
frontend/.prettierignore
Normal file
9
frontend/.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
coverage
|
||||||
|
public
|
||||||
|
.vscode
|
||||||
|
*.log
|
||||||
|
*.lock
|
||||||
9
frontend/.prettierrc
Normal file
9
frontend/.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
7
frontend/app/(auth)/login/page.tsx
Normal file
7
frontend/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
return <div> LoginPage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage
|
||||||
9
frontend/app/(protected)/dashboard/page.tsx
Normal file
9
frontend/app/(protected)/dashboard/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return (
|
||||||
|
<div>page</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default page
|
||||||
60
frontend/app/(protected)/model/page.tsx
Normal file
60
frontend/app/(protected)/model/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import ModelViewer from '@/components/ModelViewer'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [modelInfo, setModelInfo] = useState<{
|
||||||
|
meshes: unknown[]
|
||||||
|
boundingBox: {
|
||||||
|
min: { x: number; y: number; z: number }
|
||||||
|
max: { x: number; y: number; z: number }
|
||||||
|
}
|
||||||
|
} | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleModelLoaded = (data: {
|
||||||
|
meshes: unknown[]
|
||||||
|
boundingBox: {
|
||||||
|
min: { x: number; y: number; z: number }
|
||||||
|
max: { x: number; y: number; z: number }
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
setModelInfo(data)
|
||||||
|
setError(null)
|
||||||
|
console.log('Model loaded successfully:', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (errorMessage: string) => {
|
||||||
|
setError(errorMessage)
|
||||||
|
setModelInfo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-screen">
|
||||||
|
<ModelViewer
|
||||||
|
modelPath="/models/EXPO_АР_PostRecon_level.gltf"
|
||||||
|
onModelLoaded={handleModelLoaded}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="absolute top-4 right-4 left-4 z-50 rounded-lg bg-red-600/90 p-4 text-sm text-white md:right-auto md:left-4 md:w-80">
|
||||||
|
<strong>Error:</strong> {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modelInfo && (
|
||||||
|
<div className="absolute top-4 right-4 z-50 max-w-xs rounded-lg bg-black/80 p-4 text-sm text-white">
|
||||||
|
<h3 className="mb-3 text-base font-semibold">EXPO Building Model</h3>
|
||||||
|
|
||||||
|
<div className="space-y-1 text-xs text-gray-300">
|
||||||
|
<div>🖱️ Left click + drag: Rotate</div>
|
||||||
|
<div>🖱️ Right click + drag: Pan</div>
|
||||||
|
<div>🖱️ Scroll: Zoom in/out</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
frontend/app/(protected)/objects/page.tsx
Normal file
9
frontend/app/(protected)/objects/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return (
|
||||||
|
<div>page</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default page
|
||||||
1
frontend/app/default.ts
Normal file
1
frontend/app/default.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { redirect as default } from 'next/navigation'
|
||||||
131
frontend/app/hooks/useClientFetch.ts
Normal file
131
frontend/app/hooks/useClientFetch.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query'
|
||||||
|
import axios, { AxiosError, AxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||||
|
|
||||||
|
interface FetchOptions<TData, TVariables, TError> {
|
||||||
|
method?: HttpMethod
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
queryOptions?: Omit<UseQueryOptions<TData, TError>, 'queryKey' | 'queryFn'>
|
||||||
|
mutationOptions?: Omit<UseMutationOptions<TData, TError, TVariables>, 'mutationFn'>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryResult<TData, TError> {
|
||||||
|
data: TData | undefined
|
||||||
|
isLoading: boolean
|
||||||
|
error: TError | null
|
||||||
|
refetch: () => Promise<unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MutationResult<TData, TVariables, TError> {
|
||||||
|
mutate: (variables: TVariables) => void
|
||||||
|
isLoading: boolean
|
||||||
|
error: TError | null
|
||||||
|
data: TData | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClientFetch<TData = unknown, TVariables = void, TError = AxiosError>(
|
||||||
|
url: string,
|
||||||
|
options: FetchOptions<TData, TVariables, TError> = {}
|
||||||
|
): TVariables extends void
|
||||||
|
? QueryResult<TData, TError>
|
||||||
|
: MutationResult<TData, TVariables, TError> {
|
||||||
|
const { method = 'GET', config = {}, queryOptions = {}, mutationOptions = {} } = options
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||||
|
const fullUrl = url.startsWith('http') ? url : `${API_URL}${url}`
|
||||||
|
|
||||||
|
// всегда вызываем оба хука
|
||||||
|
const query = useQuery<TData, TError>({
|
||||||
|
queryKey: [url, config.params],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await axios.get(fullUrl, config)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
...queryOptions,
|
||||||
|
// отключаем автоматическое выполнение для мутаций
|
||||||
|
enabled: method === 'GET' && queryOptions.enabled !== false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mutation = useMutation<TData, TError, TVariables>({
|
||||||
|
mutationFn: async (variables: TVariables) => {
|
||||||
|
const response = await axios({
|
||||||
|
method: method.toLowerCase(),
|
||||||
|
url: fullUrl,
|
||||||
|
data: variables,
|
||||||
|
...config,
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
...mutationOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
// возвращаем соответствующий результат в зависимости от метода
|
||||||
|
if (method === 'GET') {
|
||||||
|
return {
|
||||||
|
data: query.data,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
refetch: query.refetch,
|
||||||
|
} as TVariables extends void ? QueryResult<TData, TError> : never
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mutate: mutation.mutate,
|
||||||
|
isLoading: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
data: mutation.data,
|
||||||
|
} as TVariables extends void ? never : MutationResult<TData, TVariables, TError>
|
||||||
|
}
|
||||||
|
|
||||||
|
// примеры использования:
|
||||||
|
|
||||||
|
/*
|
||||||
|
// GET запрос
|
||||||
|
interface UserData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, isLoading, error } = useClientFetch<UserData>('/users/me')
|
||||||
|
|
||||||
|
// POST запрос с типизированным payload
|
||||||
|
interface LoginPayload {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginResponse {
|
||||||
|
token: string
|
||||||
|
user: UserData
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mutate, isLoading } = useClientFetch<LoginResponse, LoginPayload>('/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
mutationOptions: {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// data типизирован как LoginResponse
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
// error типизирован как AxiosError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// использование:
|
||||||
|
mutate({ email: 'user@example.com', password: '123456' })
|
||||||
|
|
||||||
|
// PATCH запрос
|
||||||
|
interface UpdateUserPayload {
|
||||||
|
name?: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mutate } = useClientFetch<UserData, UpdateUserPayload>('/users/me', {
|
||||||
|
method: 'PATCH'
|
||||||
|
})
|
||||||
|
|
||||||
|
// использование:
|
||||||
|
mutate({ name: 'New Name' })
|
||||||
|
*/
|
||||||
150
frontend/app/hooks/useForm.ts
Normal file
150
frontend/app/hooks/useForm.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, ChangeEvent, FormEvent } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { ValidationRules, ValidationErrors } from '../types'
|
||||||
|
|
||||||
|
type FormValue = string | number | boolean | string[] | number[] | null
|
||||||
|
|
||||||
|
type CustomChangeEvent = {
|
||||||
|
target: {
|
||||||
|
id: string
|
||||||
|
value: FormValue
|
||||||
|
type?: string
|
||||||
|
checked?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useForm<T extends Record<string, FormValue>>(
|
||||||
|
initialValues: T,
|
||||||
|
validationRules?: { [K in keyof T]?: ValidationRules },
|
||||||
|
onSubmit?: (values: T) => void
|
||||||
|
) {
|
||||||
|
const [values, setValues] = useState<T>(initialValues)
|
||||||
|
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | CustomChangeEvent
|
||||||
|
) => {
|
||||||
|
const { id, value, type } = e.target
|
||||||
|
const isCheckbox = type === 'checkbox' && 'checked' in e.target
|
||||||
|
|
||||||
|
setValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[id]: isCheckbox ? (e.target as HTMLInputElement).checked : value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// скидываем ошибки
|
||||||
|
if (errors[id]) {
|
||||||
|
setErrors(prev => {
|
||||||
|
const newErrors = { ...prev }
|
||||||
|
delete newErrors[id]
|
||||||
|
return newErrors
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetField = (fieldName: keyof T, value: FormValue = '') => {
|
||||||
|
setValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[fieldName]: value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// очищаем ошибки для этого поля, если они есть
|
||||||
|
if (errors[fieldName as string]) {
|
||||||
|
setErrors(prev => {
|
||||||
|
const newErrors = { ...prev }
|
||||||
|
delete newErrors[fieldName as string]
|
||||||
|
return newErrors
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldNames: { [key: string]: string } = {
|
||||||
|
login: 'Логин',
|
||||||
|
password: 'Пароль',
|
||||||
|
accountType: 'Роль',
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
if (!validationRules) return true
|
||||||
|
|
||||||
|
const newErrors: ValidationErrors = {}
|
||||||
|
|
||||||
|
Object.keys(validationRules).forEach(key => {
|
||||||
|
const value = values[key]
|
||||||
|
const rules = validationRules[key as keyof T]
|
||||||
|
|
||||||
|
if (rules?.required && !value) {
|
||||||
|
newErrors[key] = 'Это поле обязательно'
|
||||||
|
toast.error(`Поле "${fieldNames[key] || key}" обязательно для заполнения`, {
|
||||||
|
duration: 2000,
|
||||||
|
position: 'top-right',
|
||||||
|
style: {
|
||||||
|
background: '#FEE2E2',
|
||||||
|
color: '#991B1B',
|
||||||
|
padding: '16px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules?.minLength && typeof value === 'string' && value.length < rules.minLength) {
|
||||||
|
newErrors[key] = `Минимальная длина ${rules.minLength} символов`
|
||||||
|
toast.error(
|
||||||
|
`Минимальная длина поля "${fieldNames[key] || key}" - ${rules.minLength} символов`,
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
position: 'top-right',
|
||||||
|
style: {
|
||||||
|
background: '#FEE2E2',
|
||||||
|
color: '#991B1B',
|
||||||
|
padding: '16px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules?.pattern && typeof value === 'string' && !rules.pattern.test(value)) {
|
||||||
|
newErrors[key] = 'Неверный формат'
|
||||||
|
toast.error(`Поле "${fieldNames[key] || key}" заполнено некорректно`, {
|
||||||
|
duration: 2000,
|
||||||
|
position: 'top-right',
|
||||||
|
style: {
|
||||||
|
background: '#FEE2E2',
|
||||||
|
color: '#991B1B',
|
||||||
|
padding: '16px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setErrors(newErrors)
|
||||||
|
return Object.keys(newErrors).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (validate() && onSubmit) {
|
||||||
|
onSubmit(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePasswordVisibility = () => {
|
||||||
|
setIsVisible(!isVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
isVisible,
|
||||||
|
setValues,
|
||||||
|
handleChange,
|
||||||
|
handleSubmit,
|
||||||
|
togglePasswordVisibility,
|
||||||
|
resetField,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +1,5 @@
|
|||||||
'use client'
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import ModelViewer from '../components/ModelViewer'
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [modelInfo, setModelInfo] = useState<{
|
redirect('/objects')
|
||||||
meshes: unknown[]
|
|
||||||
boundingBox: {
|
|
||||||
min: { x: number; y: number; z: number }
|
|
||||||
max: { x: number; y: number; z: number }
|
|
||||||
}
|
|
||||||
} | null>(null)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const handleModelLoaded = (data: {
|
|
||||||
meshes: unknown[]
|
|
||||||
boundingBox: {
|
|
||||||
min: { x: number; y: number; z: number }
|
|
||||||
max: { x: number; y: number; z: number }
|
|
||||||
}
|
|
||||||
}) => {
|
|
||||||
setModelInfo(data)
|
|
||||||
setError(null)
|
|
||||||
console.log('Model loaded successfully:', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleError = (errorMessage: string) => {
|
|
||||||
setError(errorMessage)
|
|
||||||
setModelInfo(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-screen relative">
|
|
||||||
<ModelViewer
|
|
||||||
modelPath="/models/EXPO_АР_PostRecon_level.gltf"
|
|
||||||
onModelLoaded={handleModelLoaded}
|
|
||||||
onError={handleError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="absolute top-4 left-4 right-4 md:left-4 md:right-auto md:w-80 bg-red-600/90 text-white p-4 rounded-lg z-50 text-sm">
|
|
||||||
<strong>Error:</strong> {error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modelInfo && (
|
|
||||||
<div className="absolute top-4 right-4 bg-black/80 text-white p-4 rounded-lg z-50 text-sm max-w-xs">
|
|
||||||
<h3 className="text-base font-semibold mb-3">EXPO Building Model</h3>
|
|
||||||
|
|
||||||
<div className="text-xs text-gray-300 space-y-1">
|
|
||||||
<div>🖱️ Left click + drag: Rotate</div>
|
|
||||||
<div>🖱️ Right click + drag: Pan</div>
|
|
||||||
<div>🖱️ Scroll: Zoom in/out</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
47
frontend/app/store/userStore.ts
Normal file
47
frontend/app/store/userStore.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
import { User, UserState } from '../types'
|
||||||
|
|
||||||
|
interface UserStore extends UserState {
|
||||||
|
// состояние
|
||||||
|
isAuthenticated: boolean
|
||||||
|
user: User | null
|
||||||
|
|
||||||
|
// действия
|
||||||
|
setUser: (user: User | null) => void
|
||||||
|
setAuthenticated: (isAuthenticated: boolean) => void
|
||||||
|
logout: () => void
|
||||||
|
|
||||||
|
// асинхронные действия
|
||||||
|
//! что пользователь может делать асинхронно?
|
||||||
|
}
|
||||||
|
|
||||||
|
const useUserStore = create<UserStore>()(
|
||||||
|
persist(
|
||||||
|
set => ({
|
||||||
|
// начальное состояние
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
favorites: [],
|
||||||
|
|
||||||
|
// синхронные действия
|
||||||
|
setUser: user => set({ user }),
|
||||||
|
setAuthenticated: isAuthenticated => set({ isAuthenticated }),
|
||||||
|
logout: () =>
|
||||||
|
set({
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
//! асинхронщина?
|
||||||
|
{ name: 'user-store' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default useUserStore
|
||||||
|
|
||||||
|
// пример использования
|
||||||
|
// const { user, isAuthenticated } = useUserStore() -- получаем данные из стора
|
||||||
|
// const { setUser, setAuthenticated } = useUserStore() -- устанавливаем данные в стор
|
||||||
|
// const { logout } = useUserStore() -- выходим из пользовательского аккаунта
|
||||||
34
frontend/app/types/index.ts
Normal file
34
frontend/app/types/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export interface ValidationRules {
|
||||||
|
required?: boolean
|
||||||
|
minLength?: number
|
||||||
|
pattern?: RegExp
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationErrors {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ToastProps = {
|
||||||
|
type: 'error' | 'success' | 'loading'
|
||||||
|
message: string
|
||||||
|
action?: {
|
||||||
|
text: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
duration?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id?: number | undefined
|
||||||
|
name: string
|
||||||
|
surname: string
|
||||||
|
image?: string
|
||||||
|
email: string
|
||||||
|
account_type?: string
|
||||||
|
login: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserState {
|
||||||
|
isAuthenticated: boolean
|
||||||
|
user: User | null
|
||||||
|
}
|
||||||
9
frontend/components/ui/Selector.tsx
Normal file
9
frontend/components/ui/Selector.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Selector = () => {
|
||||||
|
return (
|
||||||
|
<div>Selector</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Selector
|
||||||
59
frontend/components/ui/ShowToast.tsx
Normal file
59
frontend/components/ui/ShowToast.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { ToastProps } from '@/app/types'
|
||||||
|
|
||||||
|
const toastStyles = {
|
||||||
|
success: {
|
||||||
|
background: 'bg-green-50',
|
||||||
|
text: 'text-green-800',
|
||||||
|
border: 'border-green-200',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
background: 'bg-red-50',
|
||||||
|
text: 'text-red-800',
|
||||||
|
border: 'border-red-200',
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
background: 'bg-blue-50',
|
||||||
|
text: 'text-blue-800',
|
||||||
|
border: 'border-blue-200',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const showToast = ({ type, message, action, duration }: ToastProps) => {
|
||||||
|
const styles = toastStyles[type]
|
||||||
|
|
||||||
|
toast.custom(
|
||||||
|
t => (
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
t.visible ? 'animate-slideIn' : 'animate-slideOut'
|
||||||
|
} pointer-events-auto rounded-2xl px-3 shadow-lg ${styles.background} ${
|
||||||
|
styles.border
|
||||||
|
} transform border transition-all duration-300 ease-in-out`}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`text-sm font-medium ${styles.text}`}>{message}</p>
|
||||||
|
{action && (
|
||||||
|
<div className="text-center">
|
||||||
|
<button
|
||||||
|
onClick={action.onClick}
|
||||||
|
className="mt-2 text-sm font-medium text-red-600 hover:text-red-800"
|
||||||
|
>
|
||||||
|
{action.text}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ duration: duration || 4000 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default showToast
|
||||||
|
|
||||||
|
//пример использования: showToast({ type: 'error', message: 'Неверный email или пароль' })
|
||||||
9
frontend/components/ui/TextInput.tsx
Normal file
9
frontend/components/ui/TextInput.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const TextInput = () => {
|
||||||
|
return (
|
||||||
|
<div>TextInput</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextInput
|
||||||
1226
frontend/package-lock.json
generated
1226
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babylonjs/core": "^6.44.0",
|
"@babylonjs/core": "^6.44.0",
|
||||||
"@babylonjs/loaders": "^6.49.0",
|
"@babylonjs/loaders": "^6.49.0",
|
||||||
|
"@tanstack/react-query": "^5.85.5",
|
||||||
|
"axios": "^1.11.0",
|
||||||
"next": "^15.4.3",
|
"next": "^15.4.3",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "19.1.0",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"react-select": "^5.10.2",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@@ -23,6 +29,8 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.3",
|
"eslint-config-next": "15.4.3",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user