modification du layout du projet
This commit is contained in:
2
Pipfile
2
Pipfile
@@ -11,6 +11,8 @@ flask-api = "*"
|
||||
pyjwt = "*"
|
||||
flask-jwt-extended = "*"
|
||||
flask-cors = "*"
|
||||
flask-restx = "*"
|
||||
sqlalchemy = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
||||
184
Pipfile.lock
generated
184
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "669b22bb2a66c8f60df267735d00d669d090508392aa6a13af139f18ecffba67"
|
||||
"sha256": "d32cc66979cc6c5146b7607b4d44ecf5aca0c98e863c8f3c4ce33ef9e5246458"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,12 +16,29 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aniso8601": {
|
||||
"hashes": [
|
||||
"sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f",
|
||||
"sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==9.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
|
||||
"sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==21.4.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||
"sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7",
|
||||
"sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"
|
||||
],
|
||||
"version": "==2021.10.8"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2022.5.18.1"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@@ -71,6 +88,75 @@
|
||||
"index": "pypi",
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"flask-restx": {
|
||||
"hashes": [
|
||||
"sha256:63c69a61999a34f1774eaccc6fc8c7f504b1aad7d56a8ec672264e52d9ac05f4",
|
||||
"sha256:96157547acaa8892adcefd8c60abf9040212ac2a8634937a82946e07b46147fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"greenlet": {
|
||||
"hashes": [
|
||||
"sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3",
|
||||
"sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711",
|
||||
"sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd",
|
||||
"sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073",
|
||||
"sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708",
|
||||
"sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67",
|
||||
"sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23",
|
||||
"sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1",
|
||||
"sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08",
|
||||
"sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd",
|
||||
"sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2",
|
||||
"sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa",
|
||||
"sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8",
|
||||
"sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40",
|
||||
"sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab",
|
||||
"sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6",
|
||||
"sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc",
|
||||
"sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b",
|
||||
"sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e",
|
||||
"sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963",
|
||||
"sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3",
|
||||
"sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d",
|
||||
"sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d",
|
||||
"sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe",
|
||||
"sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28",
|
||||
"sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3",
|
||||
"sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e",
|
||||
"sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c",
|
||||
"sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d",
|
||||
"sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0",
|
||||
"sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497",
|
||||
"sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee",
|
||||
"sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713",
|
||||
"sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58",
|
||||
"sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a",
|
||||
"sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06",
|
||||
"sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88",
|
||||
"sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965",
|
||||
"sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f",
|
||||
"sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4",
|
||||
"sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5",
|
||||
"sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c",
|
||||
"sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a",
|
||||
"sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1",
|
||||
"sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43",
|
||||
"sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627",
|
||||
"sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b",
|
||||
"sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168",
|
||||
"sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d",
|
||||
"sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5",
|
||||
"sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478",
|
||||
"sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf",
|
||||
"sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce",
|
||||
"sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c",
|
||||
"sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"
|
||||
],
|
||||
"markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
@@ -103,6 +189,14 @@
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.2"
|
||||
},
|
||||
"jsonschema": {
|
||||
"hashes": [
|
||||
"sha256:71b5e39324422543546572954ce71c67728922c104902cb7ce252e522235b33f",
|
||||
"sha256:7c6d882619340c3347a1bf7315e147e6d3dae439033ae6383d6acb908c101dfc"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.5.1"
|
||||
},
|
||||
"mariadb": {
|
||||
"hashes": [
|
||||
"sha256:166973d6cd7da5d4fe84fc9d63f8d219a660ed2c82ebf6acadc9b3dd811f51bc",
|
||||
@@ -166,11 +260,45 @@
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41",
|
||||
"sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"
|
||||
"sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf",
|
||||
"sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.0"
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c",
|
||||
"sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc",
|
||||
"sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e",
|
||||
"sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26",
|
||||
"sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec",
|
||||
"sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286",
|
||||
"sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045",
|
||||
"sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec",
|
||||
"sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8",
|
||||
"sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c",
|
||||
"sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca",
|
||||
"sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22",
|
||||
"sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a",
|
||||
"sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96",
|
||||
"sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc",
|
||||
"sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1",
|
||||
"sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07",
|
||||
"sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6",
|
||||
"sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b",
|
||||
"sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5",
|
||||
"sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.18.1"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7",
|
||||
"sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"
|
||||
],
|
||||
"version": "==2022.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -188,6 +316,48 @@
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:09c606d8238feae2f360b8742ffbe67741937eb0a05b57f536948d198a3def96",
|
||||
"sha256:166a3887ec355f7d2f12738f7fa25dc8ac541867147a255f790f2f41f614cb44",
|
||||
"sha256:16abf35af37a3d5af92725fc9ec507dd9e9183d261c2069b6606d60981ed1c6e",
|
||||
"sha256:2e885548da361aa3f8a9433db4cfb335b2107e533bf314359ae3952821d84b3e",
|
||||
"sha256:2ec89bf98cc6a0f5d1e28e3ad28e9be6f3b4bdbd521a4053c7ae8d5e1289a8a1",
|
||||
"sha256:2ecac4db8c1aa4a269f5829df7e706639a24b780d2ac46b3e485cbbd27ec0028",
|
||||
"sha256:316c7e5304dda3e3ad711569ac5d02698bbc71299b168ac56a7076b86259f7ea",
|
||||
"sha256:5041474dcab7973baa91ec1f3112049a9dd4652898d6a95a6a895ff5c58beb6b",
|
||||
"sha256:53d2d9ee93970c969bc4e3c78b1277d7129554642f6ffea039c282c7dc4577bc",
|
||||
"sha256:5864a83bd345871ad9699ce466388f836db7572003d67d9392a71998092210e3",
|
||||
"sha256:5c90ef955d429966d84326d772eb34333178737ebb669845f1d529eb00c75e72",
|
||||
"sha256:5d50cb71c1dbed70646d521a0975fb0f92b7c3f84c61fa59e07be23a1aaeecfc",
|
||||
"sha256:64678ac321d64a45901ef2e24725ec5e783f1f4a588305e196431447e7ace243",
|
||||
"sha256:64d796e9af522162f7f2bf7a3c5531a0a550764c426782797bbeed809d0646c5",
|
||||
"sha256:6cb4c4f57a20710cea277edf720d249d514e587f796b75785ad2c25e1c0fed26",
|
||||
"sha256:6e1fe00ee85c768807f2a139b83469c1e52a9ffd58a6eb51aa7aeb524325ab18",
|
||||
"sha256:6e859fa96605027bd50d8e966db1c4e1b03e7b3267abbc4b89ae658c99393c58",
|
||||
"sha256:7a052bd9f53004f8993c624c452dfad8ec600f572dd0ed0445fbe64b22f5570e",
|
||||
"sha256:81e53bd383c2c33de9d578bfcc243f559bd3801a0e57f2bcc9a943c790662e0c",
|
||||
"sha256:83cf3077712be9f65c9aaa0b5bc47bc1a44789fd45053e2e3ecd59ff17c63fe9",
|
||||
"sha256:8b20c4178ead9bc398be479428568ff31b6c296eb22e75776273781a6551973f",
|
||||
"sha256:8d07fe2de0325d06e7e73281e9a9b5e259fbd7cbfbe398a0433cbb0082ad8fa7",
|
||||
"sha256:a0ae3aa2e86a4613f2d4c49eb7da23da536e6ce80b2bfd60bbb2f55fc02b0b32",
|
||||
"sha256:af2587ae11400157753115612d6c6ad255143efba791406ad8a0cbcccf2edcb3",
|
||||
"sha256:b3db741beaa983d4cbf9087558620e7787106319f7e63a066990a70657dd6b35",
|
||||
"sha256:be094460930087e50fd08297db9d7aadaed8408ad896baf758e9190c335632da",
|
||||
"sha256:cb441ca461bf97d00877b607f132772644b623518b39ced54da433215adce691",
|
||||
"sha256:ce20f5da141f8af26c123ebaa1b7771835ca6c161225ce728962a79054f528c3",
|
||||
"sha256:d57ac32f8dc731fddeb6f5d1358b4ca5456e72594e664769f0a9163f13df2a31",
|
||||
"sha256:dce3468bf1fc12374a1a732c9efd146ce034f91bb0482b602a9311cb6166a920",
|
||||
"sha256:e12532c4d3f614678623da5d852f038ace1f01869b89f003ed6fe8c793f0c6a3",
|
||||
"sha256:e74ce103b81c375c3853b436297952ef8d7863d801dcffb6728d01544e5191b5",
|
||||
"sha256:f0394a3acfb8925db178f7728adb38c027ed7e303665b225906bfa8099dc1ce8",
|
||||
"sha256:f522214f6749bc073262529c056f7dfd660f3b5ec4180c5354d985eb7219801e",
|
||||
"sha256:fbf8c09fe9728168f8cc1b40c239eab10baf9c422c18be7f53213d70434dea43",
|
||||
"sha256:fca8322e04b2dde722fcb0558682740eebd3bd239bea7a0d0febbc190e99dc15"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.36"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
|
||||
|
||||
77
log/U10Manager.log
Normal file
77
log/U10Manager.log
Normal file
@@ -0,0 +1,77 @@
|
||||
2022-05-19 16:41:16,281 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 16:41:16,285 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 16:42:50,490 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 16:42:50,502 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 16:46:54,185 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 16:46:54,195 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 16:46:54,345 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 16:47:33,809 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 16:47:33,812 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 16:47:33,957 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:42:07,368 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:42:07,395 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:42:07,489 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:42:39,145 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:42:39,152 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:42:39,251 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:44:48,956 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:44:48,959 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:44:48,981 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:44:57,140 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:47:37,823 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:47:37,834 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:48:03,411 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:48:03,415 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:48:03,443 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:48:03,448 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:49:04,618 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:49:04,619 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:49:04,659 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:49:04,660 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:50:56,568 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:50:56,574 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:50:56,592 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:50:56,595 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:50:59,020 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:50:59,026 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:23,530 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:23,532 - U10Manager [views.login:109] - ERROR - authorization failed !
|
||||
2022-05-19 19:51:23,532 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:33,784 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:33,787 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:33,918 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:33,922 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:36,908 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:36,909 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:36,929 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:36,932 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:47,744 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:47,747 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:47,913 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:47,916 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:50,345 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:50,347 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:50,369 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:50,372 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:51:52,796 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:51:52,798 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:52:13,041 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:52:13,100 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:52:22,270 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 19:52:22,274 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 19:52:22,375 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 20:11:24,554 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 20:11:24,606 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-19 20:11:31,885 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-19 20:11:31,886 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-20 09:01:52,314 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-20 09:01:52,322 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-20 09:01:52,485 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-20 09:01:52,496 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-20 09:04:13,524 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-20 09:04:13,541 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-20 09:04:13,743 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-20 09:04:13,745 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
2022-05-20 09:21:42,458 - U10Manager [db.connect:95] - INFO - Connexion à la base de données
|
||||
2022-05-20 09:21:42,465 - U10Manager [views.post:89] - ERROR - Login and Password required !
|
||||
2022-05-20 09:21:42,465 - U10Manager [db.disconnect:112] - INFO - Déconnexion de la base de données
|
||||
0
requirements.txt
Normal file
0
requirements.txt
Normal file
9
run.py
Normal file
9
run.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : U10Manager Flask RESTful API
|
||||
|
||||
from src import app
|
||||
print("Launch Flask RESTful API Backend ...")
|
||||
application = app.create_app()
|
||||
application.run(host="0.0.0.0")
|
||||
12
setup.py
Normal file
12
setup.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='U10Manager',
|
||||
version='1.0.0',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=[
|
||||
'flask',
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : U10Manager Flask RESTful API
|
||||
|
||||
104
src/app.py
Normal file
104
src/app.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : U10Manager Flask RESTful API
|
||||
|
||||
#########################################################
|
||||
# Importation de modules externes #
|
||||
|
||||
import sys, re, os
|
||||
from pprint import pprint
|
||||
import logging as log
|
||||
from logging.config import dictConfig
|
||||
|
||||
from flask import Flask
|
||||
from flask.logging import default_handler
|
||||
from flask_cors import CORS, cross_origin
|
||||
from flask_jwt_extended import JWTManager
|
||||
|
||||
import jwt
|
||||
|
||||
from src.config import DefaultConfig
|
||||
from src.db import db
|
||||
from src.users import users
|
||||
from src.auth import auth
|
||||
|
||||
#########################################################
|
||||
# Corps principal du programme #
|
||||
|
||||
def create_app(config=None, app_name=None):
|
||||
''' create and configure the app '''
|
||||
print("Create and configure the Flask app ...")
|
||||
if app_name is None:
|
||||
app_name = DefaultConfig.PROJECT
|
||||
|
||||
# create the app
|
||||
# tells the app that configuration files are relative to the instance folder.
|
||||
app = Flask(app_name, instance_path=os.getcwd(), instance_relative_config=True)
|
||||
|
||||
configure_app(app, config)
|
||||
configure_log(app)
|
||||
configure_database(app)
|
||||
configure_blueprints(app)
|
||||
|
||||
return app
|
||||
|
||||
def configure_app(app, config=None):
|
||||
''' configure the app with conf file and/or object '''
|
||||
# load default config
|
||||
app.config.from_object(DefaultConfig)
|
||||
|
||||
if config:
|
||||
# load specific config
|
||||
app.config.from_object(config)
|
||||
|
||||
# setup the Flask-JWT-Extended extension
|
||||
jwt = JWTManager(app)
|
||||
# setup the Flask-CORS extension for handling Cross Origin Resource Sharing
|
||||
CORS(app, resources={r"/api/*": {
|
||||
"origins": ["http://localhost:4200","http://localhost:5000"],
|
||||
"supports_credentials": True
|
||||
}})
|
||||
|
||||
def configure_log(app):
|
||||
''' configure log handler '''
|
||||
if not os.path.exists(app.config['LOG_FOLDER']):
|
||||
try:
|
||||
os.makedirs(app.config['LOG_FOLDER'])
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# On vire tous les handlers
|
||||
for h in app.logger.handlers:
|
||||
app.logger.removeHandler(h)
|
||||
|
||||
# Set info level on logger, which might be overwritten by handers.
|
||||
# Suppress DEBUG messages.
|
||||
app.logger.setLevel(log.DEBUG)
|
||||
|
||||
formatter = log.Formatter('%(asctime)s - %(name)s [%(module)s.%(funcName)s:%(lineno)d] - %(levelname)s - %(message)s')
|
||||
info_log = os.path.join(app.config['LOG_FOLDER'], DefaultConfig.PROJECT + '.log')
|
||||
info_file_handler = log.handlers.RotatingFileHandler(info_log, maxBytes=100000, backupCount=10)
|
||||
info_file_handler.setLevel(log.INFO)
|
||||
info_file_handler.setFormatter(formatter)
|
||||
app.logger.addHandler(info_file_handler)
|
||||
|
||||
fl = log.StreamHandler()
|
||||
fl.setLevel(log.DEBUG)
|
||||
fl.setFormatter(formatter)
|
||||
app.logger.addHandler(fl)
|
||||
|
||||
def configure_database(app):
|
||||
''' configure database parameters '''
|
||||
# Set database parameters ...
|
||||
db.host = app.config['SQL_HOST_URI']
|
||||
db.port = app.config['SQL_PORT']
|
||||
db.user = app.config['SQL_USERNAME']
|
||||
db.password = app.config['SQL_PASSWORD']
|
||||
db.database = app.config['SQL_DATABASE']
|
||||
|
||||
def configure_blueprints(app):
|
||||
''' configure blueprints '''
|
||||
for bp in [users, auth]:
|
||||
app.register_blueprint(bp)
|
||||
|
||||
6
src/auth/__init__.py
Normal file
6
src/auth/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : Users views and models
|
||||
|
||||
from .views import auth
|
||||
123
src/auth/views.py
Normal file
123
src/auth/views.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : Auth routes
|
||||
|
||||
#########################################################
|
||||
# Importation de modules externes #
|
||||
|
||||
import sys, re, os
|
||||
import logging as log
|
||||
|
||||
from flask import Flask, Blueprint, request, abort, jsonify, current_app
|
||||
from flask_api import status
|
||||
from flask_jwt_extended import create_access_token
|
||||
from flask_jwt_extended import get_jwt
|
||||
from flask_jwt_extended import set_access_cookies
|
||||
from flask_jwt_extended import unset_jwt_cookies
|
||||
from flask_jwt_extended import get_jwt_identity
|
||||
from flask_jwt_extended import jwt_required
|
||||
from flask_restx import Api, Resource, reqparse
|
||||
|
||||
import json
|
||||
|
||||
from werkzeug.security import check_password_hash
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from src.db import db, dbmanage
|
||||
|
||||
#########################################################
|
||||
# Class et Methods #
|
||||
|
||||
auth = Blueprint('auth', __name__, url_prefix='/api/utilisateurs')
|
||||
api = Api(auth,
|
||||
version="1.0",
|
||||
title="U10Manager Flask RESTful API",
|
||||
description="Welcome to the Swagger UI documentation site!",
|
||||
doc="/ui")
|
||||
|
||||
@auth.errorhandler(HTTPException)
|
||||
def handle_exception(e):
|
||||
''' return JSON instead of HTML for HTTP errors '''
|
||||
response = e.get_response()
|
||||
# replace the body with JSON
|
||||
response.data = json.dumps({
|
||||
'code': e.code,
|
||||
'name': e.name,
|
||||
'description': e.description,
|
||||
})
|
||||
response.content_type = "application/json"
|
||||
return response
|
||||
|
||||
@auth.after_request
|
||||
def refresh_expiring_tokens(response):
|
||||
''' Using an 'after_request' callback, we refresh any token that is within
|
||||
30 minutes of expiring.'''
|
||||
try:
|
||||
exp_timestamp = get_jwt()['exp']
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
target_timestamp = datetime.datetime.timestamp(now + datetime.timedelta(minutes=30))
|
||||
### DEBUG ###
|
||||
current_app.logger.debug("exp: {} - target: {}".format(exp_timestamp, target_timestamp))
|
||||
### END DEBUG ###
|
||||
if target_timestamp > exp_timestamp:
|
||||
current_app.logger.warning("On doit recréer un token ....")
|
||||
access_token = create_access_token(identity=get_jwt_identity())
|
||||
set_access_cookies(response, access_token)
|
||||
return response
|
||||
except (RuntimeError, KeyError):
|
||||
return response
|
||||
|
||||
@auth.route('/login', methods=['POST'])
|
||||
def login():
|
||||
### DEBUG ###
|
||||
current_app.logger.debug("Request : {}".format(request))
|
||||
current_app.logger.debug("Auth {}".format(request.authorization))
|
||||
### END DEBUG ###
|
||||
auth = request.authorization
|
||||
user = None
|
||||
if not auth or not auth.username or not auth.password:
|
||||
current_app.logger.error("Login and Password required !")
|
||||
db.disconnect()
|
||||
abort(401, description='Login and Password required')
|
||||
|
||||
# On vérifie que l'utilisateur existe en base de données
|
||||
sql_statement = "SELECT * FROM utilisateur WHERE Identifiant = \"{}\"".format(auth.username)
|
||||
# execution de la requete sql
|
||||
etat, ret = db.execute(sql_statement, None, False)
|
||||
if not etat:
|
||||
db.disconnect()
|
||||
abort(500)
|
||||
else:
|
||||
if not ret:
|
||||
db.disconnect()
|
||||
abort(404)
|
||||
else:
|
||||
user = ret[0]
|
||||
|
||||
### DEBUG ###
|
||||
current_app.logger.debug("user bdd: {}".format(user))
|
||||
### END DEBUG ###
|
||||
if user and user['Actif'] and check_password_hash(user['Password'], auth.password):
|
||||
try:
|
||||
# create a new access token
|
||||
token = create_access_token(identity=user)
|
||||
content = jsonify({'message': 'login successful !'})
|
||||
# set token as cookie
|
||||
set_access_cookies(content, token)
|
||||
except Exception as e:
|
||||
current_app.logger.erreur('Erreur : {}'.format(e))
|
||||
db.disconnect()
|
||||
abort(500)
|
||||
return content
|
||||
current_app.logger.error("authorization failed !")
|
||||
db.disconnect()
|
||||
abort(401)
|
||||
|
||||
@auth.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
""" Handles HTTP requests to URL: /api/utilisateurs/logout """
|
||||
content = jsonify({'message': 'logout successful !'})
|
||||
unset_jwt_cookies(content)
|
||||
return content
|
||||
47
src/config.py
Normal file
47
src/config.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# @author: vincent.benoit@benserv.fr
|
||||
# @brief: Flask Config classes
|
||||
|
||||
import os
|
||||
import datetime
|
||||
|
||||
class BaseConfig(object):
|
||||
PROJECT = "U10Manager"
|
||||
|
||||
# Get app root path, also can use flask.root_path.
|
||||
PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
|
||||
ADMINS = ['vincent.benoit@benserv.fr']
|
||||
|
||||
LOG_FOLDER = os.path.join(os.getcwd(), 'log')
|
||||
|
||||
class DefaultConfig(BaseConfig):
|
||||
DEBUG = True
|
||||
TESTING = True
|
||||
FLASK_ENV = 'development'
|
||||
|
||||
SECRET_KEY = "thisissecret"
|
||||
|
||||
# Setup the Flask-JWT-Extended extension
|
||||
JWT_SECRET_KEY = "cdscjdsklcfqezffhrevneqggfuhmnvqnmh"
|
||||
JWT_COOKIE_SECURE = False
|
||||
JWT_TOKEN_LOCATION = ["cookies"]
|
||||
JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(hours=1)
|
||||
# Controls if Cross Site Request Forgery (CSRF) protection is enabled when using cookies
|
||||
# This should always be True in production
|
||||
JWT_COOKIE_CSRF_PROTECT = True
|
||||
JWT_CSRF_IN_COOKIES = True
|
||||
|
||||
UPLOAD_FOLDER = os.path.join(os.getcwd(),'static/img')
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'gif', 'jpeg'}
|
||||
MAX_CONTENT_LENGTH = 1 * 1024 * 1024 # 1 megabytes
|
||||
|
||||
SQL_HOST_URI = '127.0.0.1'
|
||||
SQL_PORT = 3306
|
||||
SQL_USERNAME = 'vincent'
|
||||
SQL_PASSWORD = 'malkavian'
|
||||
SQL_DATABASE = 'test1'
|
||||
168
src/db.py
Normal file
168
src/db.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : wrapper de la base de données mariadb
|
||||
|
||||
#########################################################
|
||||
# Importation de modules externes #
|
||||
|
||||
import sys, re, os
|
||||
import logging as log
|
||||
|
||||
import mariadb
|
||||
|
||||
from flask_api import status
|
||||
from flask import current_app
|
||||
|
||||
from functools import wraps
|
||||
from src.config import DefaultConfig
|
||||
|
||||
#########################################################
|
||||
# Class et Methods #
|
||||
|
||||
class BDDsql:
|
||||
_conf = {
|
||||
'host': '',
|
||||
'port': 0,
|
||||
'user': '',
|
||||
'password': '',
|
||||
'database': ''
|
||||
}
|
||||
|
||||
def __init__(self, host="0.0.0.0", port=3306, user="user1", password="pass1", database="db1"):
|
||||
''' Constructor '''
|
||||
self._conf['host']=host
|
||||
self._conf['port']=port
|
||||
self._conf['user']=user
|
||||
self._conf['password']=password
|
||||
self._conf['database']=database
|
||||
self.connected = False
|
||||
self.logger = log.getLogger(DefaultConfig.PROJECT)
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
''' getter host parameter '''
|
||||
return self._conf['host']
|
||||
|
||||
@host.setter
|
||||
def host(self, host=""):
|
||||
''' setter host parameter '''
|
||||
self._conf['host'] = host
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
''' getter port parameter '''
|
||||
return self._conf['port']
|
||||
|
||||
@port.setter
|
||||
def port(self, port=0):
|
||||
''' setter port parameter '''
|
||||
self._conf['port'] = port
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
''' getter user parameter '''
|
||||
return self._conf['user']
|
||||
|
||||
@user.setter
|
||||
def user(self, user=""):
|
||||
''' setter user parameter'''
|
||||
self._conf['user'] = user
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
''' getter password parameter '''
|
||||
return self._conf['password']
|
||||
|
||||
@password.setter
|
||||
def password(self, password=""):
|
||||
''' setter password parameter '''
|
||||
self._conf['password'] = password
|
||||
|
||||
@property
|
||||
def database(self):
|
||||
''' getter database parameter '''
|
||||
return self._conf['database']
|
||||
|
||||
@database.setter
|
||||
def database(self, database=""):
|
||||
''' setter database parameter '''
|
||||
self._conf['database'] = database
|
||||
|
||||
def connect(self):
|
||||
''' connect to database '''
|
||||
self.logger.info("Connexion à la base de données")
|
||||
self.conn = None
|
||||
self.cursor = None
|
||||
try:
|
||||
# connection for MariaDB
|
||||
self.conn = mariadb.connect(**self._conf)
|
||||
# create a connection cursor
|
||||
self.cursor = self.conn.cursor()
|
||||
self.connected = True
|
||||
except mariadb.Error as e:
|
||||
self.logger.error("Error connecting to MariaDB Platform: {}".format(e))
|
||||
content = {'error': str(e)}
|
||||
return False, content
|
||||
return True, {}
|
||||
|
||||
def disconnect(self):
|
||||
''' disconnect from database '''
|
||||
self.logger.info("Déconnexion de la base de données")
|
||||
self.conn.close()
|
||||
self.connected = False
|
||||
return
|
||||
|
||||
def execute(self, sql_statement="", data=None, commit=False):
|
||||
''' execute SQL statement and retreive datas if necessary '''
|
||||
json_data=[]
|
||||
try:
|
||||
# execute a SQL statement
|
||||
### DEBUG ###
|
||||
self.logger.debug("statement: {}".format(sql_statement))
|
||||
### END DEBUG ###
|
||||
if not data:
|
||||
self.cursor.execute(sql_statement)
|
||||
else:
|
||||
self.cursor.execute(sql_statement, data)
|
||||
if commit:
|
||||
self.conn.commit()
|
||||
except mariadb.Error as e:
|
||||
self.logger.error("Error: {}".format(e))
|
||||
content = {'error': str(e)}
|
||||
return False, content
|
||||
|
||||
if not commit:
|
||||
# serialize results into JSON
|
||||
row_headers=[x[0] for x in self.cursor.description]
|
||||
rv = self.cursor.fetchall()
|
||||
### DEBUG ###
|
||||
self.logger.debug("description: {} - datas: {}".format(self.cursor.description, rv))
|
||||
### END DEBUG ###
|
||||
for result in rv:
|
||||
json_data.append(dict(zip(row_headers,result)))
|
||||
return True, json_data
|
||||
|
||||
#########################################################
|
||||
# Decorators #
|
||||
|
||||
def dbmanage(func):
|
||||
''' decorateur de la fonction db.connect & db.disconnect '''
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# connexion à la base de données
|
||||
state, ret = db.connect()
|
||||
if not state:
|
||||
content = {'Erreur' : ret['error']}
|
||||
return content, status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
else:
|
||||
# Appel de la fonction
|
||||
ret = func(*args, **kwargs)
|
||||
# deconnexion de la base de données
|
||||
db.disconnect()
|
||||
return ret
|
||||
return wrapper
|
||||
|
||||
# Instantiate database
|
||||
db = BDDsql()
|
||||
4
src/decorators.py
Normal file
4
src/decorators.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : U10Manager Flask RESTful API
|
||||
0
src/schema.sql
Normal file
0
src/schema.sql
Normal file
6
src/users/__init__.py
Normal file
6
src/users/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : Users views and models
|
||||
|
||||
from .views import users
|
||||
@@ -1,20 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author : vincent.benoit@benserv.fr
|
||||
# @brief : Test du token JWT avec Flask
|
||||
# @brief : Users routes
|
||||
|
||||
#########################################################
|
||||
# Importation de modules externes #
|
||||
|
||||
import sys, re, os
|
||||
import logging as log
|
||||
from logging.config import dictConfig
|
||||
from pprint import pprint
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from flask import Flask, request, abort, jsonify, render_template, make_response, send_file
|
||||
from flask_cors import CORS, cross_origin
|
||||
from flask.logging import default_handler
|
||||
from flask import Flask, Blueprint, request, abort, jsonify, send_file, current_app
|
||||
from flask_api import status
|
||||
from flask_jwt_extended import create_access_token
|
||||
from flask_jwt_extended import get_jwt
|
||||
@@ -22,142 +19,19 @@ from flask_jwt_extended import set_access_cookies
|
||||
from flask_jwt_extended import unset_jwt_cookies
|
||||
from flask_jwt_extended import get_jwt_identity
|
||||
from flask_jwt_extended import jwt_required
|
||||
from flask_jwt_extended import JWTManager
|
||||
|
||||
import json
|
||||
import mariadb
|
||||
import uuid
|
||||
import datetime
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from werkzeug.security import generate_password_hash
|
||||
from werkzeug.exceptions import HTTPException, RequestEntityTooLarge
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import jwt
|
||||
|
||||
from functools import wraps
|
||||
from src.db import db, dbmanage
|
||||
|
||||
#########################################################
|
||||
# Class et Methods #
|
||||
|
||||
class BDD:
|
||||
conf = {
|
||||
'host': '127.0.0.1',
|
||||
'port': 3306,
|
||||
'user': 'vincent',
|
||||
'password': 'malkavian',
|
||||
'database': 'test1'
|
||||
}
|
||||
users = Blueprint('users', __name__, url_prefix='/api/utilisateurs')
|
||||
|
||||
def __init__(self, host="0.0.0.0", port=3306, user="user1", password="pass1", database="db1"):
|
||||
''' Constructor '''
|
||||
self.conf['host']=host
|
||||
self.conf['port']=port
|
||||
self.conf['user']=user
|
||||
self.conf['password']=password
|
||||
self.conf['database']=database
|
||||
self.connected = False
|
||||
self.logger = log.getLogger("U10Manager")
|
||||
|
||||
def connect(self):
|
||||
''' connect to database '''
|
||||
self.logger.info("Connexion à la base de données")
|
||||
self.conn = None
|
||||
self.cursor = None
|
||||
try:
|
||||
# connection for MariaDB
|
||||
self.conn = mariadb.connect(**self.conf)
|
||||
# create a connection cursor
|
||||
self.cursor = self.conn.cursor()
|
||||
self.connected = True
|
||||
except mariadb.Error as e:
|
||||
self.logger.error("Error connecting to MariaDB Platform: {}".format(e))
|
||||
content = {'error': str(e)}
|
||||
return False, content
|
||||
return True, {}
|
||||
|
||||
def disconnect(self):
|
||||
''' disconnect from database '''
|
||||
self.logger.info("Déconnexion de la base de données")
|
||||
self.conn.close()
|
||||
self.connected = False
|
||||
return
|
||||
|
||||
def execute(self, sql_statement="", data=None, commit=False):
|
||||
''' execute SQL statement and retreive datas if necessary '''
|
||||
json_data=[]
|
||||
try:
|
||||
# execute a SQL statement
|
||||
### DEBUG ###
|
||||
self.logger.debug("statement: {}".format(sql_statement))
|
||||
### END DEBUG ###
|
||||
if not data:
|
||||
self.cursor.execute(sql_statement)
|
||||
else:
|
||||
self.cursor.execute(sql_statement, data)
|
||||
if commit:
|
||||
self.conn.commit()
|
||||
except mariadb.Error as e:
|
||||
self.logger.error("Error: {}".format(e))
|
||||
content = {'error': str(e)}
|
||||
return False, content
|
||||
|
||||
if not commit:
|
||||
# serialize results into JSON
|
||||
row_headers=[x[0] for x in self.cursor.description]
|
||||
rv = self.cursor.fetchall()
|
||||
self.logger.debug("description: {} - datas: {}".format(self.cursor.description, rv))
|
||||
for result in rv:
|
||||
json_data.append(dict(zip(row_headers,result)))
|
||||
self.logger.debug("json_data: {}".format(pprint(json_data)))
|
||||
return True, json_data
|
||||
|
||||
#########################################################
|
||||
# Corps principal du programme #
|
||||
|
||||
logger = log.getLogger("U10Manager")
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/api/*": {
|
||||
"origins": "http://localhost:4200",
|
||||
"supports_credentials": True
|
||||
}})
|
||||
db = BDD(host='127.0.0.1', port=3306, user='vincent', password='malkavian', database='test1')
|
||||
|
||||
app.config["SECRET_KEY"] = "thisissecret"
|
||||
# Setup the Flask-JWT-Extended extension
|
||||
app.config["JWT_SECRET_KEY"] = "cdscjdsklcfqezffhrevneqggfuhmnvqnmh"
|
||||
app.config["JWT_COOKIE_SECURE"] = False
|
||||
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
|
||||
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = datetime.timedelta(hours=1)
|
||||
# Controls if Cross Site Request Forgery (CSRF) protection is enabled when using cookies
|
||||
# This should always be True in production
|
||||
app.config["JWT_COOKIE_CSRF_PROTECT"] = True
|
||||
|
||||
UPLOAD_FOLDER = 'static/img'
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'gif', 'jpeg'}
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 1 * 1000 * 1000 # 1 megabytes
|
||||
|
||||
jwt = JWTManager(app)
|
||||
|
||||
def dbmanage(func):
|
||||
''' decorateur de la fonction db.connect & db.disconnect '''
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# connexion à la base de données
|
||||
state, ret = db.connect()
|
||||
if not state:
|
||||
content = {'Erreur' : ret['error']}
|
||||
return content, status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
else:
|
||||
# Appel de la fonction
|
||||
ret = func(*args, **kwargs)
|
||||
# deconnexion de la base de données
|
||||
db.disconnect()
|
||||
return ret
|
||||
return wrapper
|
||||
|
||||
@app.errorhandler(HTTPException)
|
||||
@users.errorhandler(HTTPException)
|
||||
def handle_exception(e):
|
||||
''' return JSON instead of HTML for HTTP errors '''
|
||||
response = e.get_response()
|
||||
@@ -170,7 +44,7 @@ def handle_exception(e):
|
||||
response.content_type = "application/json"
|
||||
return response
|
||||
|
||||
@app.after_request
|
||||
@users.after_request
|
||||
def refresh_expiring_tokens(response):
|
||||
''' Using an 'after_request' callback, we refresh any token that is within
|
||||
30 minutes of expiring.'''
|
||||
@@ -179,17 +53,18 @@ def refresh_expiring_tokens(response):
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
target_timestamp = datetime.datetime.timestamp(now + datetime.timedelta(minutes=30))
|
||||
### DEBUG ###
|
||||
logger.debug("exp: {} - target: {}".format(exp_timestamp, target_timestamp))
|
||||
current_app.logger.debug("exp: {} - target: {}".format(exp_timestamp, target_timestamp))
|
||||
### END DEBUG ###
|
||||
if target_timestamp > exp_timestamp:
|
||||
logger.warning("On doit recréer un token ....")
|
||||
current_app.logger.warning("On doit recréer un token ....")
|
||||
access_token = create_access_token(identity=get_jwt_identity())
|
||||
set_access_cookies(response, access_token)
|
||||
return response
|
||||
except (RuntimeError, KeyError):
|
||||
return response
|
||||
|
||||
@app.route('/api/utilisateurs', methods=['GET'])
|
||||
|
||||
@users.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def get_all_users():
|
||||
@@ -197,7 +72,7 @@ def get_all_users():
|
||||
# Access the identity of the current user with get_jwt_identity
|
||||
current_user = get_jwt_identity()
|
||||
### DEBUG ###
|
||||
logger.debug("current_user: {}".format(current_user))
|
||||
current_app.logger.debug("current_user: {}".format(current_user))
|
||||
### DEBUG END ###
|
||||
# Test si l'utilisateur courant est actif ou pas
|
||||
if not current_user["Actif"]:
|
||||
@@ -216,7 +91,7 @@ def get_all_users():
|
||||
content = ret
|
||||
return jsonify(content)
|
||||
|
||||
@app.route('/api/utilisateurs/<int:userId>', methods=['GET'])
|
||||
@users.route('/<int:userId>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def get_one_user(userId):
|
||||
@@ -224,7 +99,7 @@ def get_one_user(userId):
|
||||
# Access the identity of the current user with get_jwt_identity
|
||||
current_user = get_jwt_identity()
|
||||
### DEBUG ###
|
||||
logger.debug("Actif ? {} - Role ? {} - userId : {}/{}".format(current_user["Actif"], current_user["Role"], current_user["userId"], userId))
|
||||
current_app.logger.debug("Actif ? {} - Role ? {} - userId : {}/{}".format(current_user["Actif"], current_user["Role"], current_user["userId"], userId))
|
||||
### DEBUG END ###
|
||||
# Test si l'utilisateur courant est actif ou pas
|
||||
# Si l'utilisateur courant n'est pas administrateur, il ne peut voir que son profil
|
||||
@@ -244,7 +119,7 @@ def get_one_user(userId):
|
||||
content = ret[0]
|
||||
return jsonify(content)
|
||||
|
||||
@app.route('/api/utilisateurs', methods=['POST'])
|
||||
@users.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def add_user():
|
||||
@@ -270,7 +145,7 @@ def add_user():
|
||||
db.disconnect()
|
||||
abort(401, description="Identifiant déjà utilisé!")
|
||||
### DEBUG ###
|
||||
logger.debug("Datas: {}".format(data_json))
|
||||
current_app.logger.debug("Datas: {}".format(data_json))
|
||||
### END DEBUG ###
|
||||
# Hash du mot de passe
|
||||
hashed_password = generate_password_hash(data_json['Password'], method='sha256')
|
||||
@@ -282,12 +157,12 @@ def add_user():
|
||||
if not etat:
|
||||
content = {'Erreur': ret['error']}
|
||||
### DEBUG ###
|
||||
logger.debug("content: {}".format(content))
|
||||
current_app.logger.debug("content: {}".format(content))
|
||||
### END DEBUG ###
|
||||
return content, status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
return jsonify({'message' : 'Nouvel utilisateur créé!'})
|
||||
|
||||
@app.route('/api/utilisateurs/<int:userId>', methods=['PUT'])
|
||||
@users.route('/<int:userId>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def modif_user(userId):
|
||||
@@ -302,7 +177,7 @@ def modif_user(userId):
|
||||
|
||||
# test des attributs (JSON) de la requete
|
||||
if not request.data.decode("utf-8"):
|
||||
logger.error("Data not found")
|
||||
current_app.logger.error("Data not found")
|
||||
db.disconnect()
|
||||
abort(400, description='Data not found')
|
||||
|
||||
@@ -310,9 +185,6 @@ def modif_user(userId):
|
||||
dataDict = request.get_json()
|
||||
# Hash du mot de passe
|
||||
dataDict['Password'] = generate_password_hash(dataDict['Password'], method='sha256')
|
||||
### DEBUG ###
|
||||
logger.debug("Datas: {}".format(pprint(dataDict)))
|
||||
### END DEBUG ###
|
||||
sql_statement = "UPDATE utilisateur SET Nom=%s, Prenom=%s, Photo=%s, Identifiant=%s, Password=%s, Role=%s, Actif=%d WHERE userId = {}".format(userId)
|
||||
data = (dataDict['Nom'], dataDict['Prenom'], dataDict['Photo'], dataDict['Identifiant'], dataDict['Password'], dataDict['Role'], dataDict['Actif'])
|
||||
|
||||
@@ -323,7 +195,7 @@ def modif_user(userId):
|
||||
abort(500)
|
||||
return jsonify({'message' : 'utilisateur {} modifié!'.format(userId)})
|
||||
|
||||
@app.route('/api/utilisateurs/<int:userId>', methods=['DELETE'])
|
||||
@users.route('/<int:userId>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def del_user(userId):
|
||||
@@ -337,7 +209,7 @@ def del_user(userId):
|
||||
|
||||
# test des attributs (JSON) de la requete
|
||||
if request.data.decode("utf-8"):
|
||||
logger.error("Data found : {}".format(request.data.decode("utf-8")))
|
||||
current_app.logger.error("Data found : {}".format(request.data.decode("utf-8")))
|
||||
db.disconnect()
|
||||
abort(400)
|
||||
# On vérifie que l'utilisateur existe en base de données
|
||||
@@ -355,7 +227,7 @@ def del_user(userId):
|
||||
# On supprime l'utilisateur si celui-ci a été trouvé
|
||||
sql_statement = "DELETE FROM utilisateur WHERE userId = {}".format(userId)
|
||||
### DEBUG ###
|
||||
logger.debug("statement: {}".format(sql_statement))
|
||||
current_app.logger.debug("statement: {}".format(sql_statement))
|
||||
### END DEBUG ###
|
||||
# Execution de la requete SQL
|
||||
etat, ret = db.execute(sql_statement, None, True)
|
||||
@@ -365,7 +237,7 @@ def del_user(userId):
|
||||
content = {'message' : 'utilisateur supprimé!'}
|
||||
return jsonify(content)
|
||||
|
||||
@app.route('/api/utilisateurs/<int:userId>/reset_password', methods=['GET'])
|
||||
@users.route('/<int:userId>/reset_password', methods=['GET'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def reset_passwd_user(userId):
|
||||
@@ -389,7 +261,38 @@ def reset_passwd_user(userId):
|
||||
content = {'message' : 'reset du mot de passe!'}
|
||||
return jsonify(content)
|
||||
|
||||
@app.route('/api/utilisateurs/<int:userId>/photo', methods=['GET'])
|
||||
@users.route('/<int:userId>/activate', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def deactivate_user(userId):
|
||||
''' Désactivation d'un utilisateur représenté par son Id '''
|
||||
# Access the identity of the current user with get_jwt_identity
|
||||
current_user = get_jwt_identity()
|
||||
# Test si l'utilisateur courant est Admin ou pas
|
||||
if not current_user["Actif"] or current_user["Role"] != 'Administrateur':
|
||||
db.disconnect()
|
||||
abort(403, description='Utilisateur non autorisé')
|
||||
|
||||
# test des attributs (JSON) de la requete
|
||||
if not request.data.decode("utf-8"):
|
||||
current_app.logger.error("Data not found")
|
||||
db.disconnect()
|
||||
abort(400, description='Data not found')
|
||||
|
||||
# recuperation des attributs (JSON) de la requete
|
||||
dataDict = request.get_json()
|
||||
# Création de la requete SQL
|
||||
sql_statement = "UPDATE utilisateur SET Actif={} WHERE userId = {}".format(dataDict['Actif'], userId)
|
||||
|
||||
# Execution de la requete SQL
|
||||
etat, ret = db.execute(sql_statement, None, True)
|
||||
if not etat:
|
||||
db.disconnect()
|
||||
abort(500)
|
||||
content = {'message' : 'desactivation de l\'utilisateur réussi!'}
|
||||
return jsonify(content)
|
||||
|
||||
@users.route('/<int:userId>/photo', methods=['GET'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def get_photo(userId):
|
||||
@@ -417,10 +320,10 @@ def get_photo(userId):
|
||||
abort(404)
|
||||
user = ret[0]
|
||||
if user['Photo']:
|
||||
return send_file(os.path.join('../', user['Photo']), mimetype='image/'+os.path.splitext(user['Photo'])[1].split('.')[1])
|
||||
return send_file(os.path.join(user['Photo']), mimetype='image/'+os.path.splitext(user['Photo'])[1].split('.')[1])
|
||||
return abort(404, description='Picture not found!')
|
||||
|
||||
@app.route('/api/utilisateurs/current', methods=['GET'])
|
||||
@users.route('/current', methods=['GET'])
|
||||
@jwt_required()
|
||||
def current_user():
|
||||
''' retourne l'utilisateur courant connecté '''
|
||||
@@ -428,63 +331,10 @@ def current_user():
|
||||
current_user = get_jwt_identity()
|
||||
return jsonify(current_user)
|
||||
|
||||
@app.route('/api/utilisateurs/login', methods=['POST'])
|
||||
@dbmanage
|
||||
def login():
|
||||
### DEBUG ###
|
||||
logger.debug("Request : {}".format(request))
|
||||
logger.debug("Auth {}".format(request.authorization))
|
||||
### END DEBUG ###
|
||||
auth = request.authorization
|
||||
user = None
|
||||
if not auth or not auth.username or not auth.password:
|
||||
logger.error("Login and Password required !")
|
||||
db.disconnect()
|
||||
abort(401, description='Login and Password required')
|
||||
|
||||
# On vérifie que l'utilisateur existe en base de données
|
||||
sql_statement = "SELECT * FROM utilisateur WHERE Identifiant = \"{}\"".format(auth.username)
|
||||
# execution de la requete sql
|
||||
etat, ret = db.execute(sql_statement, None, False)
|
||||
if not etat:
|
||||
db.disconnect()
|
||||
abort(500)
|
||||
else:
|
||||
if not ret:
|
||||
db.disconnect()
|
||||
abort(404)
|
||||
else:
|
||||
user = ret[0]
|
||||
|
||||
### DEBUG ###
|
||||
logger.debug("user bdd: {}".format(user))
|
||||
### END DEBUG ###
|
||||
if user and check_password_hash(user['Password'], auth.password):
|
||||
try:
|
||||
# create a new access token
|
||||
token = create_access_token(identity=user)
|
||||
content = jsonify({'message': 'login successful !'})
|
||||
# set token as cookie
|
||||
set_access_cookies(content, token)
|
||||
except Exception as e:
|
||||
logger.erreur('Erreur : {}'.format(e))
|
||||
db.disconnect()
|
||||
return abort(500)
|
||||
return content
|
||||
logger.error("authorization failed !")
|
||||
db.disconnect()
|
||||
return abort(401)
|
||||
|
||||
@app.route('/api/utilisateurs/logout', methods=['POST'])
|
||||
def logout():
|
||||
content = jsonify({'message': 'logout successful !'})
|
||||
unset_jwt_cookies(content)
|
||||
return content
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
@app.route('/api/utilisateurs/<int:userId>/uploadImage', methods=['POST'])
|
||||
@users.route('/<int:userId>/uploadImage', methods=['POST'])
|
||||
@jwt_required()
|
||||
@dbmanage
|
||||
def uploadImage(userId):
|
||||
@@ -494,7 +344,7 @@ def uploadImage(userId):
|
||||
# Si l'utilisateur courant n'est pas administrateur, il ne peut voir que son profil
|
||||
if not current_user["Actif"] or current_user["Role"] != "Administrateur" and current_user['userId'] != userId:
|
||||
db.disconnect()
|
||||
logger.error("Utilisateur non autorisé")
|
||||
current_app.logger.error("Utilisateur non autorisé")
|
||||
abort(403, description='Utilisateur non autorisé')
|
||||
|
||||
# On vérifie que l'utilisateur existe en base de données
|
||||
@@ -510,23 +360,27 @@ def uploadImage(userId):
|
||||
abort(404)
|
||||
user = ret[0]
|
||||
|
||||
current_app.logger.debug("Req Headers: {}".format(request.headers))
|
||||
current_app.logger.debug("Req Files: {}".format(request.files))
|
||||
current_app.logger.debug("Req data: {}".format(request.data))
|
||||
|
||||
# check if the post request has the file part
|
||||
if 'photo' not in request.files:
|
||||
logger.error('No file part in the request')
|
||||
current_app.logger.error('No file part in the request')
|
||||
abort(400)
|
||||
try:
|
||||
photo = request.files['photo']
|
||||
except RequestEntityTooLarge as e:
|
||||
logger.error("Fichier trop gros ...")
|
||||
current_app.logger.error("Fichier trop gros ...")
|
||||
abort(413)
|
||||
# If the user does not select a file, the browser submits an empty file without a filename
|
||||
if photo.filename == '':
|
||||
logger.error('No selected file')
|
||||
current_app.logger.error('No selected file')
|
||||
abort(401)
|
||||
if photo and allowed_file(photo.filename):
|
||||
filename = secure_filename(photo.filename)
|
||||
### DEBUG ###
|
||||
logger.debug("filename: {}".format(os.path.splitext(filename)))
|
||||
current_app.logger.debug("filename: {}".format(os.path.splitext(filename)))
|
||||
### END DEBUG ###
|
||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], user['Identifiant'] + os.path.splitext(filename)[1])
|
||||
photo.save(filepath)
|
||||
@@ -540,18 +394,3 @@ def uploadImage(userId):
|
||||
abort(500)
|
||||
content = jsonify({'message': 'photo saved successfuly!'})
|
||||
return content
|
||||
|
||||
def main():
|
||||
logger.setLevel(log.DEBUG)
|
||||
fl = log.StreamHandler()
|
||||
fl.setLevel(log.DEBUG)
|
||||
formatter = log.Formatter('%(asctime)s - %(name)s [%(module)s.%(funcName)s:%(lineno)d] - %(levelname)s - %(message)s')
|
||||
fl.setFormatter(formatter)
|
||||
logger.addHandler(fl)
|
||||
app.logger.handlers.clear()
|
||||
app.logger.addHandler(fl)
|
||||
|
||||
app.run(host="0.0.0.0", debug=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user