✨ Update all for Postgres and new techniques
This commit is contained in:
@@ -181,6 +181,8 @@ Make sure you create a "revision" of your models and that you "upgrade" your dat
|
||||
docker-compose exec backend bash
|
||||
```
|
||||
|
||||
* If you created a new model in `./backend/app/app/db_models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic.
|
||||
|
||||
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -11,6 +11,7 @@ isort = "*"
|
||||
autoflake = "*"
|
||||
flake8 = "*"
|
||||
pytest = "*"
|
||||
vulture = "*"
|
||||
|
||||
[packages]
|
||||
fastapi = "*"
|
||||
@@ -25,6 +26,11 @@ tenacity = "*"
|
||||
pydantic = "*"
|
||||
emails = "*"
|
||||
raven = "*"
|
||||
gunicorn = "*"
|
||||
jinja2 = "*"
|
||||
psycopg2-binary = "*"
|
||||
alembic = "*"
|
||||
sqlalchemy = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
322
{{cookiecutter.project_slug}}/backend/app/Pipfile.lock
generated
322
{{cookiecutter.project_slug}}/backend/app/Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0560932caf400303d4621f7725b1e723464a3e4fe00b5a3c031739d41a5ce5fe"
|
||||
"sha256": "9e6b6eaf001ef1b6097d2ecccae8151ade81f5c4ac0f02791ec2248008ddcddf"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,6 +16,13 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"alembic": {
|
||||
"hashes": [
|
||||
"sha256:16505782b229007ae905ef9e0ae6e880fddafa406f086ac7d442c1aaf712f8c2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.7"
|
||||
},
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
"sha256:16056c952e8029ce8db097edf0d7c2fe2ba9de15d30ba08aee2c5221273d8e23",
|
||||
@@ -77,40 +84,36 @@
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
|
||||
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
|
||||
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
|
||||
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
|
||||
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
|
||||
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
|
||||
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
|
||||
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
|
||||
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
|
||||
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
|
||||
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
|
||||
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
|
||||
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
|
||||
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
|
||||
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
|
||||
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
|
||||
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
|
||||
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
|
||||
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
|
||||
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
|
||||
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
|
||||
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
|
||||
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
|
||||
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
|
||||
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
|
||||
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
|
||||
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
|
||||
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
|
||||
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
|
||||
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
|
||||
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||
"sha256:0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6",
|
||||
"sha256:27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709",
|
||||
"sha256:2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a",
|
||||
"sha256:3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282",
|
||||
"sha256:358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556",
|
||||
"sha256:37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134",
|
||||
"sha256:39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d",
|
||||
"sha256:42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da",
|
||||
"sha256:5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1",
|
||||
"sha256:5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509",
|
||||
"sha256:5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1",
|
||||
"sha256:7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3",
|
||||
"sha256:7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96",
|
||||
"sha256:7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2",
|
||||
"sha256:8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28",
|
||||
"sha256:9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c",
|
||||
"sha256:9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033",
|
||||
"sha256:a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d",
|
||||
"sha256:aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea",
|
||||
"sha256:bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0",
|
||||
"sha256:bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469",
|
||||
"sha256:bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071",
|
||||
"sha256:c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719",
|
||||
"sha256:d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a",
|
||||
"sha256:df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6",
|
||||
"sha256:f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a",
|
||||
"sha256:f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf",
|
||||
"sha256:f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729"
|
||||
],
|
||||
"version": "==1.11.5"
|
||||
"version": "==1.12.1"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -172,11 +175,19 @@
|
||||
},
|
||||
"fastapi": {
|
||||
"hashes": [
|
||||
"sha256:932d7e3d13ef1541b0eeb78576c98a68f15552c44a40ae4fb5816b39184d2307",
|
||||
"sha256:b6485bfbf585c6cb944a9a12ae0c29408f046c32ff0341bd46c6e2f1502d214d"
|
||||
"sha256:06225ac528daec555d5d8488828c9adc1570c0627800abc52481696b2a5e4d1f",
|
||||
"sha256:b37d74e197e6dbb54e3c397fe6dd270e477daa4b016ebb25366d6c9839aca298"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.0"
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
|
||||
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.9.0"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
@@ -198,6 +209,14 @@
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:529df9e0ecc0bad9fc2b376c3ce4796c41b482cf697b78b71aea6ebe7ca353c8",
|
||||
@@ -236,6 +255,45 @@
|
||||
],
|
||||
"version": "==4.3.1"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
|
||||
],
|
||||
"version": "==1.0.7"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"passlib": {
|
||||
"extras": [
|
||||
"bcrypt"
|
||||
@@ -254,6 +312,42 @@
|
||||
],
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:19a2d1f3567b30f6c2bb3baea23f74f69d51f0c06c2e2082d0d9c28b0733a4c2",
|
||||
"sha256:2b69cf4b0fa2716fd977aa4e1fd39af6110eb47b2bb30b4e5a469d8fbecfc102",
|
||||
"sha256:2e952fa17ba48cbc2dc063ddeec37d7dc4ea0ef7db0ac1eda8906365a8543f31",
|
||||
"sha256:348b49dd737ff74cfb5e663e18cb069b44c64f77ec0523b5794efafbfa7df0b8",
|
||||
"sha256:3d72a5fdc5f00ca85160915eb9a973cf9a0ab8148f6eda40708bf672c55ac1d1",
|
||||
"sha256:4957452f7868f43f32c090dadb4188e9c74a4687323c87a882e943c2bd4780c3",
|
||||
"sha256:5138cec2ee1e53a671e11cc519505eb08aaaaf390c508f25b09605763d48de4b",
|
||||
"sha256:587098ca4fc46c95736459d171102336af12f0d415b3b865972a79c03f06259f",
|
||||
"sha256:5b79368bcdb1da4a05f931b62760bea0955ee2c81531d8e84625df2defd3f709",
|
||||
"sha256:5cf43807392247d9bc99737160da32d3fa619e0bfd85ba24d1c78db205f472a4",
|
||||
"sha256:676d1a80b1eebc0cacae8dd09b2fde24213173bf65650d22b038c5ed4039f392",
|
||||
"sha256:6b0211ecda389101a7d1d3df2eba0cf7ffbdd2480ca6f1d2257c7bd739e84110",
|
||||
"sha256:79cde4660de6f0bb523c229763bd8ad9a93ac6760b72c369cf1213955c430934",
|
||||
"sha256:7aba9786ac32c2a6d5fb446002ed936b47d5e1f10c466ef7e48f66eb9f9ebe3b",
|
||||
"sha256:7c8159352244e11bdd422226aa17651110b600d175220c451a9acf795e7414e0",
|
||||
"sha256:945f2eedf4fc6b2432697eb90bb98cc467de5147869e57405bfc31fa0b824741",
|
||||
"sha256:96b4e902cde37a7fc6ab306b3ac089a3949e6ce3d824eeca5b19dc0bedb9f6e2",
|
||||
"sha256:9a7bccb1212e63f309eb9fab47b6eaef796f59850f169a25695b248ca1bf681b",
|
||||
"sha256:a3bfcac727538ec11af304b5eccadbac952d4cca1a551a29b8fe554e3ad535dc",
|
||||
"sha256:b19e9f1b85c5d6136f5a0549abdc55dcbd63aba18b4f10d0d063eb65ef2c68b4",
|
||||
"sha256:b664011bb14ca1f2287c17185e222f2098f7b4c857961dbcf9badb28786dbbf4",
|
||||
"sha256:bde7959ef012b628868d69c474ec4920252656d0800835ed999ba5e4f57e3e2e",
|
||||
"sha256:cb095a0657d792c8de9f7c9a0452385a309dfb1bbbb3357d6b1e216353ade6ca",
|
||||
"sha256:d16d42a1b9772152c1fe606f679b2316551f7e1a1ce273e7f808e82a136cdb3d",
|
||||
"sha256:d444b1545430ffc1e7a24ce5a9be122ccd3b135a7b7e695c5862c5aff0b11159",
|
||||
"sha256:d93ccc7bf409ec0a23f2ac70977507e0b8a8d8c54e5ee46109af2f0ec9e411f3",
|
||||
"sha256:df6444f952ca849016902662e1a47abf4fa0678d75f92fd9dd27f20525f809cd",
|
||||
"sha256:e63850d8c52ba2b502662bf3c02603175c2397a9acc756090e444ce49508d41e",
|
||||
"sha256:ec43358c105794bc2b6fd34c68d27f92bea7102393c01889e93f4b6a70975728",
|
||||
"sha256:f4c6926d9c03dadce7a3b378b40d2fea912c1344ef9b29869f984fb3d2a2420b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.7"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
@@ -283,6 +377,14 @@
|
||||
],
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"python-editor": {
|
||||
"hashes": [
|
||||
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
|
||||
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
|
||||
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
|
||||
],
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
||||
@@ -320,11 +422,18 @@
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:7dede29f121071da9873e7b8c98091874617858e790dc364ffaab4b09d81216c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0b3"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:7cc05c33d00db3b2ddfd7516a737544ed0a34c9dd0ced94076f29b581ce4f532"
|
||||
"sha256:9d48b35d1fc7521d59ae53c421297ab3878d3c7cd4b75266d77f6c73cccb78bb"
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
"version": "==0.11.1"
|
||||
},
|
||||
"tenacity": {
|
||||
"hashes": [
|
||||
@@ -343,25 +452,25 @@
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:e84fc3b1e142cec395fb7c1d1a9f3cdc0d455037b96e1bed54b378db1121aaba"
|
||||
"sha256:f27889a332ee5c55b4841b11b2392d00dac079f39063fabc1e13e18ada3eb7ba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.3"
|
||||
"version": "==0.4.5"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
"sha256:0ff2e67b693f7d2007466952dbe312075098e8f15364fda27d16e8a7f266d74d",
|
||||
"sha256:2d0029314dc87312ff8d46c3724363d847e5235403eced5d3f98da80a87f4828",
|
||||
"sha256:32dcc003e1973f3db303494f5f63db11091c86a146053773d81ac5484b10c416",
|
||||
"sha256:4301871418f967d0b13409f1bd10ecc7825a7f183282dcc9e19d08532e6cb2e9",
|
||||
"sha256:7639188ff4466d86cfd4418cd784d1198a8cc913279fb8798a4b12a4d42ad341",
|
||||
"sha256:a73649cd043f5d3e3ae471667c790a7ee2295b22fac7bedcae8705158f8ba111",
|
||||
"sha256:afdf34bf507090e4c7f5108a17240982760356b8aae4edd37180ec4f94c36cbb",
|
||||
"sha256:bd7a6db5dbfae0c93e27cb200bb2b9513e21a90a2d4a259b39a9b5446c4d5aa3",
|
||||
"sha256:cc27e903da274f76826848832f62e1ec410a43602e1e0cd4f8db8c619b1ee93e",
|
||||
"sha256:ec521d14ddcdd9f8d0075d7d1f82e9d8806f7f0a047d2e5bc737e9eddf7f930d"
|
||||
"sha256:198fe0c196056930ec6c4a0a878e531a66d15467ca7c74a875aa90271f0c6e3f",
|
||||
"sha256:1c175f47d34b84e33c0e312f4987c927ea004afc3a5f05d2f0f610d71d0e4c89",
|
||||
"sha256:1c47f197be8f0a3c651dd20be1e1bd43268186246f246d4e86c91e95a89e4865",
|
||||
"sha256:3fd4943570d20e8cd4d9f0a3190ebd5cf040e5610b685e05c878128a11f7ad14",
|
||||
"sha256:435e232869923fd2248e4ca0ad73e24a5b4debf40bed9dcde133cfe1bef98a7a",
|
||||
"sha256:9cfdb966ae804c46b96c92207dfd2174935ffc70e706e42e1c94c60d16dbe860",
|
||||
"sha256:a585781443eeb2edb858f8c08c503aac237a5f1bebf0c84ea8340cc337afa408",
|
||||
"sha256:b296493e033846e46488a6aa227a75c790091f5ee5456ec637bb0badad1e8851",
|
||||
"sha256:c684047c6cf6d697ba37872fb1b4489012ea91f3f802c8fbb9c367c4902e88dc",
|
||||
"sha256:da5a59d8812188b57b5783c7fb78891d14dd1050b6259680e0dbd4253d7d0f64"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
"version": "==0.12.1"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
@@ -478,11 +587,11 @@
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:c3ba1e130c813191db95c431a18cb4d20a468e98af7a77e2181b68574481ad36",
|
||||
"sha256:fd9ddf503110bf3d8b1d270e8c673aab29ccb3dd6abf29bae1f54e5116ab4a91"
|
||||
"sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048",
|
||||
"sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.5"
|
||||
"version": "==3.7.6"
|
||||
},
|
||||
"ipykernel": {
|
||||
"hashes": [
|
||||
@@ -493,11 +602,11 @@
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12",
|
||||
"sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742"
|
||||
"sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39",
|
||||
"sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"
|
||||
],
|
||||
"markers": "python_version >= '3.3'",
|
||||
"version": "==7.2.0"
|
||||
"version": "==7.3.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
@@ -534,6 +643,7 @@
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"jsonschema": {
|
||||
@@ -622,11 +732,11 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
||||
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
||||
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
||||
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
|
||||
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
|
||||
],
|
||||
"version": "==5.0.0"
|
||||
"markers": "python_version > '2.7'",
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
@@ -672,10 +782,10 @@
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6",
|
||||
"sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94"
|
||||
"sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9",
|
||||
"sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"
|
||||
],
|
||||
"version": "==0.3.3"
|
||||
"version": "==0.3.4"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
@@ -701,17 +811,17 @@
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
"sha256:e8c11ff5ca53de6c3d91e1510500611cafd1d247a937ec6c588a0a7cc3bef93c"
|
||||
"sha256:1b38b958750f66f208bcd9ab92a633c0c994d8859c831f7abc1f46724fcee490"
|
||||
],
|
||||
"version": "==0.5.0"
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:88002cc618cacfda8760c4539e76c3b3f148ecdb7035a3d422c7ecdc90c2a3ba",
|
||||
"sha256:c6655a12e9b08edb8cf5aeab4815fd1e1bdea4ad73d3bbf269cf2e0c4eb75d5e",
|
||||
"sha256:df5835fb8f417aa55e5cafadbaeb0cf630a1e824aad16989f9f0493e679ec010"
|
||||
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
|
||||
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
|
||||
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
|
||||
],
|
||||
"version": "==2.0.8"
|
||||
"version": "==2.0.9"
|
||||
},
|
||||
"ptyprocess": {
|
||||
"hashes": [
|
||||
@@ -757,11 +867,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
|
||||
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
|
||||
"sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
|
||||
"sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.0"
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@@ -772,33 +882,33 @@
|
||||
},
|
||||
"pyzmq": {
|
||||
"hashes": [
|
||||
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8",
|
||||
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349",
|
||||
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba",
|
||||
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946",
|
||||
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf",
|
||||
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46",
|
||||
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59",
|
||||
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786",
|
||||
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40",
|
||||
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7",
|
||||
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d",
|
||||
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3",
|
||||
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d",
|
||||
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce",
|
||||
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca",
|
||||
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1",
|
||||
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed",
|
||||
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf",
|
||||
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27",
|
||||
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8",
|
||||
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810",
|
||||
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b",
|
||||
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19",
|
||||
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192",
|
||||
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"
|
||||
"sha256:07a03450418694fb07e76a0191b6bc9f411afc8e364ca2062edcf28bb0e51c63",
|
||||
"sha256:15f0bf7cd80020f165635595e197603aedb37fddf4164ad5ae226afc43242f7b",
|
||||
"sha256:1756dc72e192c670490e38c788c3a35f901adc74ee436e5131d5a3e85fdd7dc6",
|
||||
"sha256:1d1eb490da54679d724b08ef3ee04530849023670c4ba7e400ed2cdf906720c4",
|
||||
"sha256:228402625796821f08706f58cc42a3c51c9897d723550babaefe4feec2b6dacc",
|
||||
"sha256:264ac9dcee6a7af2bce4b61f2d19e5926118a5caa629b50f107ef6318670a364",
|
||||
"sha256:2b5a43da65f5dec857184d5c2ce13b80071019e96358f146bdecff7238765bc9",
|
||||
"sha256:3928534fa00a2aabfcfdb439c08ba37fbe99ab0cf57776c8db8d2b73a51693ba",
|
||||
"sha256:3d2a295b1086d450981f73d3561ac204a0cc9c8ded386a4a34327d918f3b1d0a",
|
||||
"sha256:411def5b4cbe6111856040a55c8048df113882e90c57ce88de4a48f0189441ac",
|
||||
"sha256:4b77e96a7ffc1c5e08eaf274db554f227b31717d086adca1bb42b12ef35a7194",
|
||||
"sha256:4c87fa3e449e1f4ab9170cdfe8213dc0ba34a11b160e6adecafa892e451a29b6",
|
||||
"sha256:4fd8621a309db6ec23ef1369f43cdf7a9b0dc217d8ff9ca4095a6e932b379bda",
|
||||
"sha256:54fe55a1694ffe608c8e4c5183e83cab7a91f3e5c84bd6f188868d6676c12aba",
|
||||
"sha256:60acabd86808a16a895a247fd2bf7a127284a33562d79687bb5df163cff068b2",
|
||||
"sha256:618887be4ad754228c0cbba7631f6574608b4430fe93974e6322324f1304fdac",
|
||||
"sha256:69130efb6efa936de601cb135a8a4eec1caccd4ea2b784237145ff4075c2d3ae",
|
||||
"sha256:6e7f78eeac82140bde7e60e975c6e6b1b678a4dd377782ab63319c1c78bf3aa1",
|
||||
"sha256:6ee760cdb84e43574da6b3f2f1fc1251e8acf87253900d28a06451c5f5de39e9",
|
||||
"sha256:75c87f1dc1e65cea4b709f2ebc78fa18d4b475e41463502aec9cd26208b88e0f",
|
||||
"sha256:97cb1b7cd2c46e87b0a26651eccd2bbb8c758035efd1635ebb81ac36aa76a88c",
|
||||
"sha256:abfa774dbadacc849121ed92eae05189d226daab583388b499472e1bbb17ef69",
|
||||
"sha256:ae3d2627d74195ddc95675f2f814aca998381b73dc4341b9e10e3e191e1bdb0b",
|
||||
"sha256:b30c339eb58355f51f4f54dd61d785f1ff58c86bca1c3a5916977631d121867b",
|
||||
"sha256:cbabdced5b137cd56aa22633f13ac5690029a0ad43ab6c05f53206e489178362"
|
||||
],
|
||||
"version": "==17.1.2"
|
||||
"version": "==18.0.0"
|
||||
},
|
||||
"qtconsole": {
|
||||
"hashes": [
|
||||
@@ -844,9 +954,9 @@
|
||||
},
|
||||
"tornado": {
|
||||
"hashes": [
|
||||
"sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82"
|
||||
"sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671"
|
||||
],
|
||||
"version": "==6.0a1"
|
||||
"version": "==6.0b1"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
@@ -879,6 +989,14 @@
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"vulture": {
|
||||
"hashes": [
|
||||
"sha256:4b5a8980c338e9c068d43e7164555a1e4c9c7d84961ce2bc6f3ed975f6e5bc9d",
|
||||
"sha256:524b6b9642d0bbe74ea21478bf260937d1ba9b3b86676ca0b17cd10b4b51ba01"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
|
||||
74
{{cookiecutter.project_slug}}/backend/app/alembic.ini
Executable file
74
{{cookiecutter.project_slug}}/backend/app/alembic.ini
Executable file
@@ -0,0 +1,74 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# timezone to use when rendering the date
|
||||
# within the migration file as well as the filename.
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = postgresql://postgres:changethis@db/app
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1,42 @@
|
||||
"""First revision
|
||||
|
||||
Revision ID: e6ae69e9dcb9
|
||||
Revises:
|
||||
Create Date: 2019-02-13 14:27:57.038583
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e6ae69e9dcb9'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('full_name', sa.String(), nullable=True),
|
||||
sa.Column('email', sa.String(), nullable=True),
|
||||
sa.Column('hashed_password', sa.String(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_superuser', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
||||
op.create_index(op.f('ix_user_full_name'), 'user', ['full_name'], unique=False)
|
||||
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_user_id'), table_name='user')
|
||||
op.drop_index(op.f('ix_user_full_name'), table_name='user')
|
||||
op.drop_index(op.f('ix_user_email'), table_name='user')
|
||||
op.drop_table('user')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,12 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.api_v1.endpoints.role import router as roles_router
|
||||
from app.api.api_v1.endpoints.token import router as token_router
|
||||
from app.api.api_v1.endpoints.user import router as user_router
|
||||
from app.api.api_v1.endpoints.utils import router as utils_router
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(roles_router)
|
||||
api_router.include_router(token_router)
|
||||
api_router.include_router(user_router)
|
||||
api_router.include_router(utils_router)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from app.core.jwt import get_current_user
|
||||
from app.crud.user import check_if_user_is_active, check_if_user_is_superuser
|
||||
from app.crud.utils import ensure_enums_to_strs
|
||||
from app.models.role import RoleEnum, Roles
|
||||
from app.models.user import UserInDB
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/roles/", response_model=Roles)
|
||||
def route_roles_get(current_user: UserInDB = Depends(get_current_user)):
|
||||
"""
|
||||
Retrieve roles
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not (check_if_user_is_superuser(current_user)):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The current user does not have enogh privileges"
|
||||
)
|
||||
roles = ensure_enums_to_strs(RoleEnum)
|
||||
return {"roles": roles}
|
||||
@@ -1,22 +1,19 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from starlette.exceptions import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.utils.db import get_db
|
||||
from app.api.utils.security import get_current_user
|
||||
from app.core import config
|
||||
from app.core.jwt import create_access_token, get_current_user
|
||||
from app.crud.user import (
|
||||
authenticate_user,
|
||||
check_if_user_is_active,
|
||||
check_if_user_is_superuser,
|
||||
get_user,
|
||||
update_user,
|
||||
)
|
||||
from app.db.database import get_default_bucket
|
||||
from app.core.jwt import create_access_token
|
||||
from app.core.security import get_password_hash
|
||||
from app.crud import user as crud_user
|
||||
from app.db_models.user import User as DBUser
|
||||
from app.models.msg import Msg
|
||||
from app.models.token import Token
|
||||
from app.models.user import User, UserInDB, UserInUpdate
|
||||
from app.models.user import User
|
||||
from app.utils import (
|
||||
generate_password_reset_token,
|
||||
send_reset_password_email,
|
||||
@@ -27,70 +24,73 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login/access-token", response_model=Token, tags=["login"])
|
||||
def route_login_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
def login_access_token(
|
||||
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
|
||||
):
|
||||
"""
|
||||
OAuth2 compatible token login, get an access token for future requests
|
||||
"""
|
||||
bucket = get_default_bucket()
|
||||
user = authenticate_user(bucket, form_data.username, form_data.password)
|
||||
user = crud_user.authenticate(
|
||||
db, email=form_data.username, password=form_data.password
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
elif not check_if_user_is_active(user):
|
||||
elif not crud_user.is_active(user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return {
|
||||
"access_token": create_access_token(
|
||||
data={"username": form_data.username}, expires_delta=access_token_expires
|
||||
data={"user_id": user.id}, expires_delta=access_token_expires
|
||||
),
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/login/test-token", tags=["login"], response_model=User)
|
||||
def route_test_token(current_user: UserInDB = Depends(get_current_user)):
|
||||
def test_token(current_user: DBUser = Depends(get_current_user)):
|
||||
"""
|
||||
Test access token
|
||||
"""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/password-recovery/{username}", tags=["login"], response_model=Msg)
|
||||
def route_recover_password(username: str):
|
||||
@router.post("/password-recovery/{email}", tags=["login"], response_model=Msg)
|
||||
def recover_password(email: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Password Recovery
|
||||
"""
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
user = crud_user.get_by_email(db, email=email)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system.",
|
||||
)
|
||||
password_reset_token = generate_password_reset_token(username)
|
||||
password_reset_token = generate_password_reset_token(email=email)
|
||||
send_reset_password_email(
|
||||
email_to=user.email, username=username, token=password_reset_token
|
||||
email_to=user.email, email=email, token=password_reset_token
|
||||
)
|
||||
return {"msg": "Password recovery email sent"}
|
||||
|
||||
|
||||
@router.post("/reset-password/", tags=["login"], response_model=Msg)
|
||||
def route_reset_password(token: str, new_password: str):
|
||||
def reset_password(token: str, new_password: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Reset password
|
||||
"""
|
||||
username = verify_password_reset_token(token)
|
||||
if not username:
|
||||
email = verify_password_reset_token(token)
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="Invalid token")
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
user = crud_user.get_by_email(db, email=email)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system.",
|
||||
)
|
||||
elif not check_if_user_is_active(user):
|
||||
elif not crud_user.is_active(user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
user_in = UserInUpdate(name=username, password=new_password)
|
||||
user = update_user(bucket, user_in)
|
||||
hashed_password = get_password_hash(new_password)
|
||||
user.hashed_password = hashed_password
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return {"msg": "Password updated successfully"}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Body, Depends
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic.types import EmailStr
|
||||
from starlette.exceptions import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.utils.db import get_db
|
||||
from app.api.utils.security import get_current_user
|
||||
from app.core import config
|
||||
from app.core.jwt import get_current_user
|
||||
from app.crud.user import (
|
||||
check_if_user_is_active,
|
||||
check_if_user_is_superuser,
|
||||
get_user,
|
||||
get_users,
|
||||
search_users,
|
||||
update_user,
|
||||
upsert_user,
|
||||
)
|
||||
from app.db.database import get_default_bucket
|
||||
from app.crud import user as crud_user
|
||||
from app.db_models.user import User as DBUser
|
||||
from app.models.user import User, UserInCreate, UserInDB, UserInUpdate
|
||||
from app.utils import send_new_account_email
|
||||
|
||||
@@ -23,116 +17,99 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/users/", tags=["users"], response_model=List[User])
|
||||
def route_users_get(
|
||||
skip: int = 0, limit: int = 100, current_user: UserInDB = Depends(get_current_user)
|
||||
def read_users(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: DBUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Retrieve users
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
if not crud_user.is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
elif not crud_user.is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
users = get_users(bucket, skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
|
||||
@router.get("/users/search/", tags=["users"], response_model=List[User])
|
||||
def route_search_users(
|
||||
q: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Search users, use Bleve Query String syntax: http://blevesearch.com/docs/Query-String-Query/
|
||||
|
||||
For typeahead sufix with `*`. For example, a query with: `email:johnd*` will match users with
|
||||
email `johndoe@example.com`, `johndid@example.net`, etc.
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
users = search_users(bucket=bucket, query_string=q, skip=skip, limit=limit)
|
||||
users = crud_user.get_multi(db, skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
|
||||
@router.post("/users/", tags=["users"], response_model=User)
|
||||
def route_users_post(
|
||||
*, user_in: UserInCreate, current_user: UserInDB = Depends(get_current_user)
|
||||
def create_user(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
user_in: UserInCreate,
|
||||
current_user: DBUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create new user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
if not crud_user.is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
elif not crud_user.is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, user_in.username)
|
||||
user = crud_user.get_by_email(db, email=user_in.email)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this username already exists in the system.",
|
||||
)
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user = crud_user.create(db, user_in=user_in)
|
||||
if config.EMAILS_ENABLED and user_in.email:
|
||||
send_new_account_email(
|
||||
email_to=user_in.email, username=user_in.username, password=user_in.password
|
||||
email_to=user_in.email, username=user_in.email, password=user_in.password
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/users/me", tags=["users"], response_model=User)
|
||||
def route_users_me_put(
|
||||
def update_user_me(
|
||||
*,
|
||||
password: str = None,
|
||||
full_name: str = None,
|
||||
email: EmailStr = None,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
password: str = Body(None),
|
||||
full_name: str = Body(None),
|
||||
email: EmailStr = Body(None),
|
||||
current_user: DBUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update own user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
if not crud_user.is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
user_in = UserInUpdate(**current_user.dict())
|
||||
current_user_data = jsonable_encoder(current_user)
|
||||
user_in = UserInUpdate(**current_user_data)
|
||||
if password is not None:
|
||||
user_in.password = password
|
||||
if full_name is not None:
|
||||
user_in.full_name = full_name
|
||||
if email is not None:
|
||||
user_in.email = email
|
||||
bucket = get_default_bucket()
|
||||
user = update_user(bucket, user_in)
|
||||
user = crud_user.update(db, user=current_user, user_in=user_in)
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/users/me", tags=["users"], response_model=User)
|
||||
def route_users_me_get(current_user: UserInDB = Depends(get_current_user)):
|
||||
def read_user_me(
|
||||
db: Session = Depends(get_db), current_user: DBUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get current user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
if not crud_user.is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/users/open", tags=["users"], response_model=User)
|
||||
def route_users_post_open(
|
||||
def create_user_open(
|
||||
*,
|
||||
username: str = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
password: str = Body(...),
|
||||
email: EmailStr = Body(None),
|
||||
email: EmailStr = Body(...),
|
||||
full_name: str = Body(None),
|
||||
):
|
||||
"""
|
||||
@@ -143,63 +120,61 @@ def route_users_post_open(
|
||||
status_code=403,
|
||||
detail="Open user resgistration is forbidden on this server",
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
user = crud_user.get_by_email(db, email=email)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this username already exists in the system",
|
||||
)
|
||||
user_in = UserInCreate(
|
||||
username=username, password=password, email=email, full_name=full_name
|
||||
)
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_in = UserInCreate(password=password, email=email, full_name=full_name)
|
||||
user = crud_user.create(db, user_in=user_in)
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/users/{username}", tags=["users"], response_model=User)
|
||||
def route_users_id_get(
|
||||
username: str, current_user: UserInDB = Depends(get_current_user)
|
||||
@router.get("/users/{user_id}", tags=["users"], response_model=User)
|
||||
def read_user_by_id(
|
||||
user_id: int,
|
||||
current_user: DBUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get a specific user by username (email)
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
if not crud_user.is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
user = crud_user.get(db, user_id=user_id)
|
||||
if user == current_user:
|
||||
return user
|
||||
if not check_if_user_is_superuser(current_user):
|
||||
if not crud_user.is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/users/{username}", tags=["users"], response_model=User)
|
||||
def route_users_put(
|
||||
@router.put("/users/{user_id}", tags=["users"], response_model=User)
|
||||
def update_user(
|
||||
*,
|
||||
username: str,
|
||||
db: Session = Depends(get_db),
|
||||
user_id: int,
|
||||
user_in: UserInUpdate,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update a user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
if not crud_user.is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
elif not crud_user.is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
user = crud_user.get(db, user_id=user_id)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system",
|
||||
)
|
||||
user = update_user(bucket, user_in)
|
||||
user = crud_user.update(db, user=user, user_in=user_in)
|
||||
return user
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic.types import EmailStr
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from app.api.utils.security import get_current_user
|
||||
from app.core.celery_app import celery_app
|
||||
from app.core.jwt import get_current_user
|
||||
from app.crud.user import check_if_user_is_superuser
|
||||
from app.crud import user as crud_user
|
||||
from app.models.msg import Msg
|
||||
from app.models.user import UserInDB
|
||||
from app.utils import send_test_email
|
||||
@@ -13,24 +12,22 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/test-celery/", tags=["utils"], response_model=Msg, status_code=201)
|
||||
def route_test_celery(msg: Msg, current_user: UserInDB = Depends(get_current_user)):
|
||||
def test_celery(msg: Msg, current_user: UserInDB = Depends(get_current_user)):
|
||||
"""
|
||||
Test Celery worker
|
||||
"""
|
||||
if not check_if_user_is_superuser(current_user):
|
||||
if not crud_user.is_superuser(current_user):
|
||||
raise HTTPException(status_code=400, detail="Not a superuser")
|
||||
celery_app.send_task("app.worker.test_celery", args=[msg.msg])
|
||||
return {"msg": "Word received"}
|
||||
|
||||
|
||||
@router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201)
|
||||
def route_test_email(
|
||||
email_to: EmailStr, current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
def test_email(email_to: EmailStr, current_user: UserInDB = Depends(get_current_user)):
|
||||
"""
|
||||
Test emails
|
||||
"""
|
||||
if not check_if_user_is_superuser(current_user):
|
||||
if not crud_user.is_superuser(current_user):
|
||||
raise HTTPException(status_code=400, detail="Not a superuser")
|
||||
send_test_email(email_to=email_to)
|
||||
return {"msg": "Test email sent"}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from starlette.requests import Request
|
||||
|
||||
|
||||
def get_db(request: Request):
|
||||
return request.state.db
|
||||
@@ -0,0 +1,30 @@
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, Security
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jwt import PyJWTError
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
from app.api.utils.db import get_db
|
||||
from app.core import config
|
||||
from app.core.jwt import ALGORITHM
|
||||
from app.crud import user as crud_user
|
||||
from app.models.token import TokenPayload
|
||||
|
||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
|
||||
|
||||
|
||||
def get_current_user(
|
||||
db: Session = Depends(get_db), token: str = Security(reusable_oauth2)
|
||||
):
|
||||
try:
|
||||
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
|
||||
token_data = TokenPayload(**payload)
|
||||
except PyJWTError:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
user = crud_user.get(db, user_id=token_data.user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
||||
|
||||
from app.db.external_session import db_session
|
||||
from app.db.session import db_session
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
||||
|
||||
from app.db.external_session import db_session
|
||||
from app.db.session import db_session
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,7 +21,7 @@ SERVER_NAME = os.getenv("SERVER_NAME")
|
||||
SERVER_HOST = os.getenv("SERVER_HOST")
|
||||
BACKEND_CORS_ORIGINS = os.getenv(
|
||||
"BACKEND_CORS_ORIGINS"
|
||||
) # a string of origins separated by commas, e.g: "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://dev.couchbase-project.com, https://stag.couchbase-project.com, https://couchbase-project.com, http://local.dockertoolbox.tiangolo.com"
|
||||
) # a string of origins separated by commas, e.g: "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://local.dockertoolbox.tiangolo.com"
|
||||
PROJECT_NAME = os.getenv("PROJECT_NAME")
|
||||
SENTRY_DSN = os.getenv("SENTRY_DSN")
|
||||
|
||||
@@ -47,8 +47,6 @@ EMAIL_RESET_TOKEN_EXPIRE_HOURS = 48
|
||||
EMAIL_TEMPLATES_DIR = "/app/app/email-templates/build"
|
||||
EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL
|
||||
|
||||
ROLE_SUPERUSER = "superuser"
|
||||
|
||||
FIRST_SUPERUSER = os.getenv("FIRST_SUPERUSER")
|
||||
FIRST_SUPERUSER_PASSWORD = os.getenv("FIRST_SUPERUSER_PASSWORD")
|
||||
|
||||
|
||||
@@ -1,37 +1,12 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from fastapi import Security
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jwt import PyJWTError
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
from app.core.config import SECRET_KEY
|
||||
from app.crud.user import get_user
|
||||
from app.db.database import get_default_bucket
|
||||
from app.models.token import TokenPayload
|
||||
from app.core import config
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
access_token_jwt_subject = "access"
|
||||
|
||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
|
||||
|
||||
|
||||
def get_current_user(token: str = Security(reusable_oauth2)):
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
token_data = TokenPayload(**payload)
|
||||
except PyJWTError:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username=token_data.username)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
||||
to_encode = data.copy()
|
||||
@@ -40,5 +15,5 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire, "sub": access_token_jwt_subject})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@@ -3,9 +3,9 @@ from passlib.context import CryptContext
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
def verify_password(plain_password: str, hashed_password: str):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
def get_password_hash(password: str):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
@@ -1,81 +1,47 @@
|
||||
from app.core.security import get_password_hash
|
||||
from app.models.role import Role
|
||||
from app.models.user import User
|
||||
from typing import List, Union
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.db_models.user import User
|
||||
from app.models.user import UserInCreate, UserInUpdate
|
||||
|
||||
|
||||
def get_user(username, db_session):
|
||||
return db_session.query(User).filter(User.id == username).first()
|
||||
def get(db_session, *, user_id: int) -> Union[User, None]:
|
||||
return db_session.query(User).filter(User.id == user_id).first()
|
||||
|
||||
|
||||
def check_if_user_is_active(user):
|
||||
def get_by_email(db_session, *, email: str) -> Union[User, None]:
|
||||
return db_session.query(User).filter(User.email == email).first()
|
||||
|
||||
|
||||
def authenticate(db_session, *, email: str, password: str) -> Union[User, bool]:
|
||||
user = get_by_email(db_session, email=email)
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
def is_active(user) -> bool:
|
||||
return user.is_active
|
||||
|
||||
|
||||
def check_if_user_is_superuser(user):
|
||||
def is_superuser(user) -> bool:
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
def check_if_username_is_active(username, db_session):
|
||||
user = get_user(username, db_session)
|
||||
return check_if_user_is_active(user)
|
||||
def get_multi(db_session, *, skip=0, limit=100) -> Union[List[User], List[None]]:
|
||||
return db_session.query(User).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def get_role_by_name(name, db_session):
|
||||
role = db_session.query(Role).filter(Role.name == name).first()
|
||||
return role
|
||||
|
||||
|
||||
def get_role_by_id(role_id, db_session):
|
||||
role = db_session.query(Role).filter(Role.id == role_id).first()
|
||||
return role
|
||||
|
||||
|
||||
def create_role(name, db_session):
|
||||
role = Role(name=name)
|
||||
db_session.add(role)
|
||||
db_session.commit()
|
||||
return role
|
||||
|
||||
|
||||
def get_roles(db_session):
|
||||
return db_session.query(Role).all()
|
||||
|
||||
|
||||
def get_user_roles(user):
|
||||
return user.roles
|
||||
|
||||
|
||||
def get_user_by_username(username, db_session) -> User:
|
||||
user = db_session.query(User).filter(User.email == username).first() # type: User
|
||||
return user
|
||||
|
||||
|
||||
def get_user_by_id(user_id, db_session):
|
||||
user = db_session.query(User).filter(User.id == user_id).first() # type: User
|
||||
return user
|
||||
|
||||
|
||||
def get_user_hashed_password(user):
|
||||
return user.password
|
||||
|
||||
|
||||
def get_user_id(user):
|
||||
return user.id
|
||||
|
||||
|
||||
def get_users(db_session):
|
||||
return db_session.query(User).all()
|
||||
|
||||
|
||||
def create_user(
|
||||
db_session, username, password, first_name=None, last_name=None, is_superuser=False
|
||||
):
|
||||
def create(db_session, *, user_in: UserInCreate) -> User:
|
||||
user = User(
|
||||
email=username,
|
||||
password=get_password_hash(password),
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
is_superuser=is_superuser,
|
||||
email=user_in.email,
|
||||
hashed_password=get_password_hash(user_in.password),
|
||||
full_name=user_in.full_name,
|
||||
is_superuser=user_in.is_superuser,
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
@@ -83,8 +49,16 @@ def create_user(
|
||||
return user
|
||||
|
||||
|
||||
def assign_role_to_user(role: Role, user: User, db_session):
|
||||
user.roles.append(role)
|
||||
def update(db_session, *, user: User, user_in: UserInUpdate) -> User:
|
||||
user_data = jsonable_encoder(user)
|
||||
for field in user_data:
|
||||
if field in user_in.fields:
|
||||
value_in = getattr(user_in, field)
|
||||
if value_in is not None:
|
||||
setattr(user, field, value_in)
|
||||
if user_in.password:
|
||||
passwordhash = get_password_hash(user_in.password)
|
||||
user.hashed_password = passwordhash
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import List, Sequence, Type, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field, Shape
|
||||
|
||||
from app.core.config import COUCHBASE_BUCKET_NAME
|
||||
from couchbase.bucket import Bucket
|
||||
from couchbase.fulltext import MatchAllQuery, QueryStringQuery
|
||||
from couchbase.n1ql import CONSISTENCY_REQUEST, N1QLQuery
|
||||
|
||||
|
||||
def generate_new_id():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def ensure_enums_to_strs(items: Union[Sequence[Union[Enum, str]], Type[Enum]]):
|
||||
str_items = []
|
||||
for item in items:
|
||||
if isinstance(item, Enum):
|
||||
str_items.append(str(item.value))
|
||||
else:
|
||||
str_items.append(str(item))
|
||||
return str_items
|
||||
|
||||
|
||||
def get_all_documents_by_type(bucket: Bucket, *, doc_type: str, skip=0, limit=100):
|
||||
query_str = f"SELECT *, META().id as id FROM {COUCHBASE_BUCKET_NAME} WHERE type = $type LIMIT $limit OFFSET $skip;"
|
||||
q = N1QLQuery(
|
||||
query_str, bucket=COUCHBASE_BUCKET_NAME, type=doc_type, limit=limit, skip=skip
|
||||
)
|
||||
q.consistency = CONSISTENCY_REQUEST
|
||||
result = bucket.n1ql_query(q)
|
||||
return result
|
||||
|
||||
|
||||
def get_documents_by_keys(
|
||||
bucket: Bucket, *, keys: List[str], doc_model=Type[BaseModel]
|
||||
):
|
||||
results = bucket.get_multi(keys, quiet=True)
|
||||
docs = []
|
||||
for result in results.values():
|
||||
doc = doc_model(**result.value)
|
||||
docs.append(doc)
|
||||
return docs
|
||||
|
||||
|
||||
def results_to_model(results_from_couchbase: list, *, doc_model: Type[BaseModel]):
|
||||
items = []
|
||||
for doc in results_from_couchbase:
|
||||
data = doc[COUCHBASE_BUCKET_NAME]
|
||||
doc = doc_model(**data)
|
||||
items.append(doc)
|
||||
return items
|
||||
|
||||
|
||||
def search_results_to_model(
|
||||
results_from_couchbase: list, *, doc_model: Type[BaseModel]
|
||||
):
|
||||
items = []
|
||||
for doc in results_from_couchbase:
|
||||
data = doc.get("fields")
|
||||
if not data:
|
||||
continue
|
||||
data_nones = {}
|
||||
for key, value in data.items():
|
||||
field: Field = doc_model.__fields__[key]
|
||||
if not value:
|
||||
value = None
|
||||
elif field.shape in {Shape.LIST, Shape.SET, Shape.TUPLE} and not isinstance(
|
||||
value, list
|
||||
):
|
||||
value = [value]
|
||||
data_nones[key] = value
|
||||
doc = doc_model(**data_nones)
|
||||
items.append(doc)
|
||||
return items
|
||||
|
||||
|
||||
def get_docs(
|
||||
bucket: Bucket, *, doc_type: str, doc_model=Type[BaseModel], skip=0, limit=100
|
||||
):
|
||||
doc_results = get_all_documents_by_type(
|
||||
bucket, doc_type=doc_type, skip=skip, limit=limit
|
||||
)
|
||||
return results_to_model(doc_results, doc_model=doc_model)
|
||||
|
||||
|
||||
def get_doc(bucket: Bucket, *, doc_id: str, doc_model: Type[BaseModel]):
|
||||
result = bucket.get(doc_id, quiet=True)
|
||||
if not result.value:
|
||||
return None
|
||||
model = doc_model(**result.value)
|
||||
return model
|
||||
|
||||
|
||||
def search_docs_get_doc_ids(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
query = QueryStringQuery(query_string)
|
||||
hits = bucket.search(index_name, query, skip=skip, limit=limit)
|
||||
doc_ids = []
|
||||
for hit in hits:
|
||||
doc_ids.append(hit["id"])
|
||||
return doc_ids
|
||||
|
||||
|
||||
def search_get_results(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
if query_string:
|
||||
query = QueryStringQuery(query_string)
|
||||
else:
|
||||
query = MatchAllQuery()
|
||||
hits = bucket.search(index_name, query, fields=["*"], skip=skip, limit=limit)
|
||||
docs = []
|
||||
for hit in hits:
|
||||
docs.append(hit)
|
||||
return docs
|
||||
|
||||
|
||||
def search_get_results_by_type(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_type: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
type_filter = f"type:{doc_type}"
|
||||
if not query_string:
|
||||
query_string = type_filter
|
||||
if query_string and type_filter not in query_string:
|
||||
query_string += f" {type_filter}"
|
||||
query = QueryStringQuery(query_string)
|
||||
hits = bucket.search(index_name, query, fields=["*"], skip=skip, limit=limit)
|
||||
docs = []
|
||||
for hit in hits:
|
||||
docs.append(hit)
|
||||
return docs
|
||||
|
||||
|
||||
def search_docs(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_model: Type[BaseModel],
|
||||
skip=0,
|
||||
limit=100,
|
||||
):
|
||||
keys = search_docs_get_doc_ids(
|
||||
bucket=bucket,
|
||||
query_string=query_string,
|
||||
index_name=index_name,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
if not keys:
|
||||
return []
|
||||
doc_results = get_documents_by_keys(bucket=bucket, keys=keys, doc_model=doc_model)
|
||||
return doc_results
|
||||
|
||||
|
||||
def search_results(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_model: Type[BaseModel],
|
||||
skip=0,
|
||||
limit=100,
|
||||
):
|
||||
doc_results = search_get_results(
|
||||
bucket=bucket,
|
||||
query_string=query_string,
|
||||
index_name=index_name,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return search_results_to_model(doc_results, doc_model=doc_model)
|
||||
|
||||
|
||||
def search_results_by_type(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_type: str,
|
||||
doc_model: Type[BaseModel],
|
||||
skip=0,
|
||||
limit=100,
|
||||
):
|
||||
doc_results = search_get_results_by_type(
|
||||
bucket=bucket,
|
||||
query_string=query_string,
|
||||
index_name=index_name,
|
||||
doc_type=doc_type,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return search_results_to_model(doc_results, doc_model=doc_model)
|
||||
@@ -1,5 +1,4 @@
|
||||
# Import all the models, so that Base has them before being
|
||||
# imported by Alembic
|
||||
from app.db.base_class import Base # noqa
|
||||
from app.models.role import Role # noqa
|
||||
from app.models.user import User # noqa
|
||||
from app.db_models.user import User # noqa
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path, PurePath
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from app.core.config import (
|
||||
COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR,
|
||||
COUCHBASE_PASSWORD,
|
||||
COUCHBASE_USER,
|
||||
)
|
||||
|
||||
|
||||
def get_index(
|
||||
index_name: str,
|
||||
*,
|
||||
username: str = COUCHBASE_USER,
|
||||
password: str = COUCHBASE_PASSWORD,
|
||||
host="couchbase",
|
||||
port="8094",
|
||||
):
|
||||
full_text_url = f"http://{host}:{port}"
|
||||
index_url = f"{full_text_url}/api/index/{index_name}"
|
||||
auth = HTTPBasicAuth(username, password)
|
||||
response = requests.get(index_url, auth=auth)
|
||||
if response.status_code == 400:
|
||||
content = response.json()
|
||||
error = content.get("error")
|
||||
if error == "rest_auth: preparePerms, err: index not found":
|
||||
return None
|
||||
raise ValueError(error)
|
||||
elif response.status_code == 200:
|
||||
content = response.json()
|
||||
assert (
|
||||
content.get("status") == "ok"
|
||||
), "Expected a status OK communicating with Full Text Search"
|
||||
index_def = content.get("indexDef")
|
||||
return index_def
|
||||
raise ValueError(response.text)
|
||||
|
||||
|
||||
def create_index(
|
||||
index_definition: Dict[str, Any],
|
||||
*,
|
||||
reset_uuids=True,
|
||||
username: str = COUCHBASE_USER,
|
||||
password: str = COUCHBASE_PASSWORD,
|
||||
host="couchbase",
|
||||
port="8094",
|
||||
):
|
||||
index_name = index_definition.get("name")
|
||||
assert index_name, "An index name is required as key in an index definition"
|
||||
if reset_uuids:
|
||||
index_definition.update({"uuid": "", "sourceUUID": ""})
|
||||
full_text_url = f"http://{host}:{port}"
|
||||
index_url = f"{full_text_url}/api/index/{index_name}"
|
||||
auth = HTTPBasicAuth(username, password)
|
||||
response = requests.put(index_url, auth=auth, json=index_definition)
|
||||
content = response.json()
|
||||
if response.status_code == 400:
|
||||
error = content.get("error")
|
||||
if (
|
||||
"cannot create index because an index with the same name already exists:"
|
||||
in error
|
||||
):
|
||||
raise ValueError(error)
|
||||
else:
|
||||
raise ValueError(error)
|
||||
elif response.status_code == 200:
|
||||
assert (
|
||||
content.get("status") == "ok"
|
||||
), "Expected a status OK communicating with Full Text Search"
|
||||
return True
|
||||
raise ValueError(response.text)
|
||||
|
||||
|
||||
def ensure_create_full_text_indexes(
|
||||
index_dir=COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR,
|
||||
username: str = COUCHBASE_USER,
|
||||
password: str = COUCHBASE_PASSWORD,
|
||||
host="couchbase",
|
||||
port="8094",
|
||||
):
|
||||
file_path: PurePath
|
||||
for file_path in Path(index_dir).iterdir():
|
||||
if file_path.name.endswith(".json"):
|
||||
with open(file_path) as f:
|
||||
index_definition = json.load(f)
|
||||
name = index_definition.get("name")
|
||||
assert name, "A full text search index definition must have a name field"
|
||||
current_index = get_index(
|
||||
index_name=name,
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
if not current_index:
|
||||
assert create_index(
|
||||
index_definition=index_definition,
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
), "Full Text Search index could not be created"
|
||||
@@ -1,11 +1,6 @@
|
||||
from app.core import config
|
||||
from app.db.utils import (
|
||||
assign_role_to_user,
|
||||
create_role,
|
||||
create_user,
|
||||
get_role_by_name,
|
||||
get_user_by_username,
|
||||
)
|
||||
from app.crud import user as crud_user
|
||||
from app.models.user import UserInCreate
|
||||
|
||||
|
||||
def init_db(db_session):
|
||||
@@ -14,16 +9,11 @@ def init_db(db_session):
|
||||
# the tables uncommenting the next line
|
||||
# Base.metadata.create_all(bind=engine)
|
||||
|
||||
role = get_role_by_name("default", db_session)
|
||||
if not role:
|
||||
role = create_role("default", db_session)
|
||||
|
||||
user = get_user_by_username(config.FIRST_SUPERUSER, db_session)
|
||||
user = crud_user.get_by_email(db_session, email=config.FIRST_SUPERUSER)
|
||||
if not user:
|
||||
user = create_user(
|
||||
db_session,
|
||||
config.FIRST_SUPERUSER,
|
||||
config.FIRST_SUPERUSER_PASSWORD,
|
||||
user_in = UserInCreate(
|
||||
email=config.FIRST_SUPERUSER,
|
||||
password=config.FIRST_SUPERUSER_PASSWORD,
|
||||
is_superuser=True,
|
||||
)
|
||||
assign_role_to_user(role, user, db_session)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from app.core import config
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
|
||||
from app.core import config
|
||||
|
||||
engine = create_engine(config.SQLALCHEMY_DATABASE_URI, convert_unicode=True)
|
||||
db_session = scoped_session(
|
||||
sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
)
|
||||
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
@@ -1,11 +0,0 @@
|
||||
# Import installed packages
|
||||
# Import app code
|
||||
from app.db.base_class import Base
|
||||
from sqlalchemy import Column, ForeignKey, Integer, Table
|
||||
|
||||
users_roles = Table(
|
||||
"users_roles",
|
||||
Base.metadata,
|
||||
Column("user_id", Integer, ForeignKey("user.id")),
|
||||
Column("role_id", Integer, ForeignKey("role.id")),
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
# Import standard library packages
|
||||
from datetime import datetime
|
||||
|
||||
# Import app code
|
||||
from app.db.base_class import Base
|
||||
from app.models.base_relations import users_roles
|
||||
|
||||
# Import installed packages
|
||||
from sqlalchemy import Column, DateTime, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
class Role(Base):
|
||||
# Own properties
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow(), index=True)
|
||||
name = Column(String, index=True)
|
||||
# Relationships
|
||||
users = relationship("User", secondary=users_roles, back_populates="roles")
|
||||
@@ -1,29 +1,12 @@
|
||||
# Import standard library packages
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Boolean, Column, Integer, String
|
||||
|
||||
# Typings, for autocompletion (VS Code with Python plug-in)
|
||||
from typing import List # noqa
|
||||
|
||||
# Import app code
|
||||
from app.db.base_class import Base
|
||||
from app.models.base_relations import users_roles
|
||||
|
||||
# Import installed packages
|
||||
from sqlalchemy import Boolean, Column, DateTime, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
class User(Base):
|
||||
# Own properties
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow(), index=True)
|
||||
first_name = Column(String, index=True)
|
||||
last_name = Column(String, index=True)
|
||||
full_name = Column(String, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
password = Column(String)
|
||||
hashed_password = Column(String)
|
||||
is_active = Column(Boolean(), default=True)
|
||||
is_superuser = Column(Boolean(), default=False)
|
||||
# Relationships
|
||||
roles = relationship(
|
||||
"Role", secondary=users_roles, back_populates="users"
|
||||
) # type: List[role.Role]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import logging
|
||||
|
||||
from app.db.init_db import init_db
|
||||
from app.db.session import db_session
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def init():
|
||||
init_db(db_session)
|
||||
|
||||
|
||||
def main():
|
||||
logger.info("Creating initial data")
|
||||
init()
|
||||
logger.info("Initial data created")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,17 +1,19 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.api.api_v1.api import api_router
|
||||
from app.core.config import API_V1_STR, BACKEND_CORS_ORIGINS, PROJECT_NAME
|
||||
from app.core import config
|
||||
from app.db.session import Session
|
||||
|
||||
app = FastAPI(title=PROJECT_NAME, openapi_url="/api/v1/openapi.json")
|
||||
app = FastAPI(title=config.PROJECT_NAME, openapi_url="/api/v1/openapi.json")
|
||||
|
||||
# CORS
|
||||
origins = []
|
||||
|
||||
# Set all CORS enabled origins
|
||||
if BACKEND_CORS_ORIGINS:
|
||||
origins_raw = BACKEND_CORS_ORIGINS.split(",")
|
||||
if config.BACKEND_CORS_ORIGINS:
|
||||
origins_raw = config.BACKEND_CORS_ORIGINS.split(",")
|
||||
for origin in origins_raw:
|
||||
use_origin = origin.strip()
|
||||
origins.append(use_origin)
|
||||
@@ -23,4 +25,12 @@ if BACKEND_CORS_ORIGINS:
|
||||
allow_headers=["*"],
|
||||
),
|
||||
|
||||
app.include_router(api_router, prefix=API_V1_STR)
|
||||
app.include_router(api_router, prefix=config.API_V1_STR)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def db_session_middleware(request: Request, call_next):
|
||||
request.state.db = Session()
|
||||
response = await call_next(request)
|
||||
request.state.db.close()
|
||||
return response
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
USERPROFILE_DOC_TYPE = "userprofile"
|
||||
@@ -1,14 +0,0 @@
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.config import ROLE_SUPERUSER
|
||||
|
||||
|
||||
class RoleEnum(Enum):
|
||||
superuser = ROLE_SUPERUSER
|
||||
|
||||
|
||||
class Roles(BaseModel):
|
||||
roles: List[RoleEnum]
|
||||
@@ -7,4 +7,4 @@ class Token(BaseModel):
|
||||
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
username: str = None
|
||||
user_id: int = None
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
from typing import List, Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.models.config import USERPROFILE_DOC_TYPE
|
||||
from app.models.role import RoleEnum
|
||||
|
||||
|
||||
# Shared properties
|
||||
class UserBase(BaseModel):
|
||||
email: Optional[str] = None
|
||||
admin_roles: Optional[List[Union[str, RoleEnum]]] = None
|
||||
admin_channels: Optional[List[Union[str, RoleEnum]]] = None
|
||||
disabled: Optional[bool] = None
|
||||
is_active: Optional[bool] = True
|
||||
is_superuser: Optional[bool] = False
|
||||
full_name: Optional[str] = None
|
||||
|
||||
|
||||
class UserBaseInDB(UserBase):
|
||||
username: str
|
||||
full_name: Optional[str] = None
|
||||
id: int = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class UserInCreate(UserBaseInDB):
|
||||
email: str
|
||||
password: str
|
||||
admin_roles: List[Union[str, RoleEnum]] = []
|
||||
admin_channels: List[Union[str, RoleEnum]] = []
|
||||
disabled: bool = False
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
@@ -39,10 +33,4 @@ class User(UserBaseInDB):
|
||||
|
||||
# Additional properties stored in DB
|
||||
class UserInDB(UserBaseInDB):
|
||||
type: str = USERPROFILE_DOC_TYPE
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class UserSyncIn(UserBase):
|
||||
name: str
|
||||
password: Optional[str] = None
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "users",
|
||||
"type": "fulltext-alias",
|
||||
"params": {
|
||||
"targets": {
|
||||
"users_01": {}
|
||||
}
|
||||
},
|
||||
"sourceType": "nil",
|
||||
"sourceName": "",
|
||||
"sourceUUID": "",
|
||||
"sourceParams": null,
|
||||
"planParams": {},
|
||||
"uuid": ""
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"name": "users_01",
|
||||
"type": "fulltext-index",
|
||||
"params": {
|
||||
"doc_config": {
|
||||
"docid_prefix_delim": "",
|
||||
"docid_regexp": "",
|
||||
"mode": "type_field",
|
||||
"type_field": "type"
|
||||
},
|
||||
"mapping": {
|
||||
"analysis": {
|
||||
"analyzers": {
|
||||
"userprofile": {
|
||||
"token_filters": [
|
||||
"apostrophe",
|
||||
"to_lower"
|
||||
],
|
||||
"tokenizer": "unicode",
|
||||
"type": "custom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default_analyzer": "standard",
|
||||
"default_datetime_parser": "dateTimeOptional",
|
||||
"default_field": "_all",
|
||||
"default_mapping": {
|
||||
"dynamic": true,
|
||||
"enabled": false
|
||||
},
|
||||
"default_type": "_default",
|
||||
"docvalues_dynamic": true,
|
||||
"index_dynamic": true,
|
||||
"store_dynamic": false,
|
||||
"type_field": "_type",
|
||||
"types": {
|
||||
"userprofile": {
|
||||
"dynamic": false,
|
||||
"enabled": true,
|
||||
"properties": {
|
||||
"type": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"analyzer": "keyword",
|
||||
"store": false,
|
||||
"index": true,
|
||||
"include_term_vectors": false,
|
||||
"include_in_all": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"admin_channels": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "admin_channels",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"admin_roles": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "admin_roles",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"disabled": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "disabled",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"email": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "email",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"full_name": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "standard",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "full_name",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"username": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "username",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"store": {
|
||||
"indexType": "scorch",
|
||||
"kvStoreName": ""
|
||||
}
|
||||
},
|
||||
"sourceType": "couchbase",
|
||||
"sourceName": "app",
|
||||
"sourceUUID": "",
|
||||
"sourceParams": {},
|
||||
"planParams": {
|
||||
"maxPartitionsPerPIndex": 171,
|
||||
"numReplicas": 0
|
||||
},
|
||||
"uuid": ""
|
||||
}
|
||||
@@ -27,4 +27,4 @@ def test_use_access_token(superuser_token_headers):
|
||||
)
|
||||
result = r.json()
|
||||
assert r.status_code == 200
|
||||
assert "username" in result
|
||||
assert "email" in result
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import requests
|
||||
|
||||
from app.core import config
|
||||
from app.crud.user import get_user, upsert_user
|
||||
from app.db.database import get_default_bucket
|
||||
from app.crud import user as crud_user
|
||||
from app.db.session import db_session
|
||||
from app.models.user import UserInCreate
|
||||
from app.tests.utils.user import user_authentication_headers
|
||||
from app.tests.utils.utils import get_server_api, random_lower_string
|
||||
@@ -15,16 +15,16 @@ def test_get_users_superuser_me(superuser_token_headers):
|
||||
)
|
||||
current_user = r.json()
|
||||
assert current_user
|
||||
assert current_user["disabled"] is False
|
||||
assert "superuser" in current_user["admin_roles"]
|
||||
assert current_user["username"] == config.FIRST_SUPERUSER
|
||||
assert current_user["is_active"] is True
|
||||
assert current_user["is_superuser"]
|
||||
assert current_user["email"] == config.FIRST_SUPERUSER
|
||||
|
||||
|
||||
def test_create_user_new_email(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
data = {"username": username, "password": password}
|
||||
data = {"email": username, "password": password}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/users/",
|
||||
headers=superuser_token_headers,
|
||||
@@ -32,26 +32,25 @@ def test_create_user_new_email(superuser_token_headers):
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
created_user = r.json()
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
assert user.username == created_user["username"]
|
||||
user = crud_user.get_by_email(db_session, email=username)
|
||||
assert user.email == created_user["email"]
|
||||
|
||||
|
||||
def test_get_existing_user(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_in = UserInCreate(email=username, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
user_id = user.id
|
||||
r = requests.get(
|
||||
f"{server_api}{config.API_V1_STR}/users/{username}",
|
||||
f"{server_api}{config.API_V1_STR}/users/{user_id}",
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
api_user = r.json()
|
||||
user = get_user(bucket, username)
|
||||
assert user.username == api_user["username"]
|
||||
user = crud_user.get_by_email(db_session, email=username)
|
||||
assert user.email == api_user["email"]
|
||||
|
||||
|
||||
def test_create_user_existing_username(superuser_token_headers):
|
||||
@@ -59,10 +58,9 @@ def test_create_user_existing_username(superuser_token_headers):
|
||||
username = random_lower_string()
|
||||
# username = email
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
data = {"username": username, "password": password}
|
||||
user_in = UserInCreate(email=username, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
data = {"email": username, "password": password}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/users/",
|
||||
headers=superuser_token_headers,
|
||||
@@ -77,11 +75,10 @@ def test_create_user_by_normal_user():
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_in = UserInCreate(email=username, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
user_token_headers = user_authentication_headers(server_api, username, password)
|
||||
data = {"username": username, "password": password}
|
||||
data = {"email": username, "password": password}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/users/", headers=user_token_headers, json=data
|
||||
)
|
||||
@@ -92,14 +89,13 @@ def test_retrieve_users(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_in = UserInCreate(email=username, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
|
||||
username2 = random_lower_string()
|
||||
password2 = random_lower_string()
|
||||
user_in2 = UserInCreate(username=username2, email=username2, password=password2)
|
||||
user2 = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_in2 = UserInCreate(email=username2, password=password2)
|
||||
user2 = crud_user.create(db_session, user_in=user_in2)
|
||||
|
||||
r = requests.get(
|
||||
f"{server_api}{config.API_V1_STR}/users/", headers=superuser_token_headers
|
||||
@@ -108,5 +104,4 @@ def test_retrieve_users(superuser_token_headers):
|
||||
|
||||
assert len(all_users) > 1
|
||||
for user in all_users:
|
||||
assert "username" in user
|
||||
assert "admin_roles" in user
|
||||
assert "email" in user
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from app.crud.user import get_user_doc_id
|
||||
|
||||
|
||||
def test_get_user_id():
|
||||
username = "johndoe@example.com"
|
||||
user_id = get_user_doc_id(username)
|
||||
assert user_id == "userprofile::johndoe@example.com"
|
||||
@@ -1,14 +1,7 @@
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from app.crud.user import (
|
||||
authenticate_user,
|
||||
check_if_user_is_active,
|
||||
check_if_user_is_superuser,
|
||||
get_user,
|
||||
upsert_user,
|
||||
)
|
||||
from app.db.database import get_default_bucket
|
||||
from app.models.role import RoleEnum
|
||||
from app.crud import user as crud_user
|
||||
from app.db.session import db_session
|
||||
from app.models.user import UserInCreate
|
||||
from app.tests.utils.utils import random_lower_string
|
||||
|
||||
@@ -16,90 +9,75 @@ from app.tests.utils.utils import random_lower_string
|
||||
def test_create_user():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=email, email=email, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
assert hasattr(user, "username")
|
||||
assert user.username == email
|
||||
user_in = UserInCreate(email=email, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
assert user.email == email
|
||||
assert hasattr(user, "hashed_password")
|
||||
assert hasattr(user, "type")
|
||||
assert user.type == "userprofile"
|
||||
|
||||
|
||||
def test_authenticate_user():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=email, email=email, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
authenticated_user = authenticate_user(bucket, email, password)
|
||||
user_in = UserInCreate(email=email, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
authenticated_user = crud_user.authenticate(
|
||||
db_session, email=email, password=password
|
||||
)
|
||||
assert authenticated_user
|
||||
assert user.username == authenticated_user.username
|
||||
assert user.email == authenticated_user.email
|
||||
|
||||
|
||||
def test_not_authenticate_user():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
bucket = get_default_bucket()
|
||||
user = authenticate_user(bucket, email, password)
|
||||
user = crud_user.authenticate(db_session, email=email, password=password)
|
||||
assert user is False
|
||||
|
||||
|
||||
def test_check_if_user_is_active():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=email, email=email, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_active = check_if_user_is_active(user)
|
||||
user_in = UserInCreate(email=email, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
is_active = crud_user.is_active(user)
|
||||
assert is_active is True
|
||||
|
||||
|
||||
def test_check_if_user_is_active_inactive():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(
|
||||
username=email, email=email, password=password, disabled=True
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_active = check_if_user_is_active(user)
|
||||
assert is_active is False
|
||||
user_in = UserInCreate(email=email, password=password, disabled=True)
|
||||
print(user_in)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
print(user)
|
||||
is_active = crud_user.is_active(user)
|
||||
print(is_active)
|
||||
assert is_active
|
||||
|
||||
|
||||
def test_check_if_user_is_superuser():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(
|
||||
username=email, email=email, password=password, admin_roles=[RoleEnum.superuser]
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_superuser = check_if_user_is_superuser(user)
|
||||
user_in = UserInCreate(email=email, password=password, is_superuser=True)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
is_superuser = crud_user.is_superuser(user)
|
||||
assert is_superuser is True
|
||||
|
||||
|
||||
def test_check_if_user_is_superuser_normal_user():
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_superuser = check_if_user_is_superuser(user)
|
||||
user_in = UserInCreate(email=username, password=password)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
is_superuser = crud_user.is_superuser(user)
|
||||
assert is_superuser is False
|
||||
|
||||
|
||||
def test_get_user():
|
||||
password = random_lower_string()
|
||||
username = random_lower_string()
|
||||
user_in = UserInCreate(
|
||||
username=username,
|
||||
email=username,
|
||||
password=password,
|
||||
admin_roles=[RoleEnum.superuser],
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_2 = get_user(bucket, username)
|
||||
assert user.username == user_2.username
|
||||
user_in = UserInCreate(email=username, password=password, is_superuser=True)
|
||||
user = crud_user.create(db_session, user_in=user_in)
|
||||
user_2 = crud_user.get(db_session, user_id=user.id)
|
||||
assert user.email == user_2.email
|
||||
assert jsonable_encoder(user) == jsonable_encoder(user_2)
|
||||
|
||||
@@ -2,8 +2,8 @@ import logging
|
||||
|
||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
||||
|
||||
from app.db.external_session import db_session
|
||||
from app.tests.api.api_v1.token.test_token import test_get_access_token
|
||||
from app.db.session import db_session
|
||||
from app.tests.api.api_v1.test_token import test_get_access_token
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,96 +1,86 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
from typing import Optional
|
||||
|
||||
import emails
|
||||
import jwt
|
||||
from emails.template import JinjaTemplate
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
|
||||
from app.core.config import (
|
||||
EMAIL_RESET_TOKEN_EXPIRE_HOURS,
|
||||
EMAIL_TEMPLATES_DIR,
|
||||
EMAILS_ENABLED,
|
||||
EMAILS_FROM_EMAIL,
|
||||
EMAILS_FROM_NAME,
|
||||
PROJECT_NAME,
|
||||
SECRET_KEY,
|
||||
SERVER_HOST,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_PORT,
|
||||
SMTP_TLS,
|
||||
SMTP_USER,
|
||||
)
|
||||
from app.core import config
|
||||
|
||||
password_reset_jwt_subject = "preset"
|
||||
|
||||
|
||||
def send_email(email_to: str, subject_template="", html_template="", environment={}):
|
||||
assert EMAILS_ENABLED, "no provided configuration for email variables"
|
||||
assert config.EMAILS_ENABLED, "no provided configuration for email variables"
|
||||
message = emails.Message(
|
||||
subject=JinjaTemplate(subject_template),
|
||||
html=JinjaTemplate(html_template),
|
||||
mail_from=(EMAILS_FROM_NAME, EMAILS_FROM_EMAIL),
|
||||
mail_from=(config.EMAILS_FROM_NAME, config.EMAILS_FROM_EMAIL),
|
||||
)
|
||||
smtp_options = {"host": SMTP_HOST, "port": SMTP_PORT}
|
||||
if SMTP_TLS:
|
||||
smtp_options = {"host": config.SMTP_HOST, "port": config.SMTP_PORT}
|
||||
if config.SMTP_TLS:
|
||||
smtp_options["tls"] = True
|
||||
if SMTP_USER:
|
||||
smtp_options["user"] = SMTP_USER
|
||||
if SMTP_PASSWORD:
|
||||
smtp_options["password"] = SMTP_PASSWORD
|
||||
if config.SMTP_USER:
|
||||
smtp_options["user"] = config.SMTP_USER
|
||||
if config.SMTP_PASSWORD:
|
||||
smtp_options["password"] = config.SMTP_PASSWORD
|
||||
response = message.send(to=email_to, render=environment, smtp=smtp_options)
|
||||
logging.info(f"send email result: {response}")
|
||||
|
||||
|
||||
def send_test_email(email_to: str):
|
||||
subject = f"{PROJECT_NAME} - Test email"
|
||||
with open(Path(EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
|
||||
project_name = config.PROJECT_NAME
|
||||
subject = f"{project_name} - Test email"
|
||||
with open(Path(config.EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
|
||||
template_str = f.read()
|
||||
send_email(
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={"project_name": PROJECT_NAME, "email": email_to},
|
||||
environment={"project_name": config.PROJECT_NAME, "email": email_to},
|
||||
)
|
||||
|
||||
|
||||
def send_reset_password_email(email_to: str, username: str, token: str):
|
||||
subject = f"{PROJECT_NAME} - Password recovery for user {username}"
|
||||
with open(Path(EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
|
||||
def send_reset_password_email(email_to: str, email: str, token: str):
|
||||
project_name = config.PROJECT_NAME
|
||||
subject = f"{project_name} - Password recovery for user {email}"
|
||||
with open(Path(config.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
|
||||
template_str = f.read()
|
||||
if hasattr(token, "decode"):
|
||||
use_token = token.decode()
|
||||
else:
|
||||
use_token = token
|
||||
link = f"{SERVER_HOST}/reset-password?token={use_token}"
|
||||
server_host = config.SERVER_HOST
|
||||
link = f"{server_host}/reset-password?token={use_token}"
|
||||
send_email(
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={
|
||||
"project_name": PROJECT_NAME,
|
||||
"username": username,
|
||||
"project_name": config.PROJECT_NAME,
|
||||
"username": email,
|
||||
"email": email_to,
|
||||
"valid_hours": EMAIL_RESET_TOKEN_EXPIRE_HOURS,
|
||||
"valid_hours": config.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
|
||||
"link": link,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def send_new_account_email(email_to: str, username: str, password: str):
|
||||
subject = f"{PROJECT_NAME} - New acccount for user {username}"
|
||||
with open(Path(EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
|
||||
project_name = config.PROJECT_NAME
|
||||
subject = f"{project_name} - New acccount for user {username}"
|
||||
with open(Path(config.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
|
||||
template_str = f.read()
|
||||
link = f"{SERVER_HOST}"
|
||||
link = config.SERVER_HOST
|
||||
send_email(
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={
|
||||
"project_name": PROJECT_NAME,
|
||||
"project_name": config.PROJECT_NAME,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email_to,
|
||||
@@ -99,28 +89,23 @@ def send_new_account_email(email_to: str, username: str, password: str):
|
||||
)
|
||||
|
||||
|
||||
def generate_password_reset_token(username):
|
||||
delta = timedelta(hours=EMAIL_RESET_TOKEN_EXPIRE_HOURS)
|
||||
def generate_password_reset_token(email):
|
||||
delta = timedelta(hours=config.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
|
||||
now = datetime.utcnow()
|
||||
expires = now + delta
|
||||
exp = expires.timestamp()
|
||||
encoded_jwt = jwt.encode(
|
||||
{
|
||||
"exp": exp,
|
||||
"nbf": now,
|
||||
"sub": password_reset_jwt_subject,
|
||||
"username": username,
|
||||
},
|
||||
SECRET_KEY,
|
||||
{"exp": exp, "nbf": now, "sub": password_reset_jwt_subject, "email": email},
|
||||
config.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_password_reset_token(token) -> Union[str, bool]:
|
||||
def verify_password_reset_token(token) -> Optional[str]:
|
||||
try:
|
||||
decoded_token = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
||||
decoded_token = jwt.decode(token, config.SECRET_KEY, algorithms=["HS256"])
|
||||
assert decoded_token["sub"] == password_reset_jwt_subject
|
||||
return decoded_token["username"]
|
||||
return decoded_token["email"]
|
||||
except InvalidTokenError:
|
||||
return False
|
||||
return None
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
# Import standard library modules
|
||||
|
||||
|
||||
# Import installed packages
|
||||
from raven import Client
|
||||
|
||||
from app.core import config
|
||||
from app.core.celery_app import celery_app
|
||||
|
||||
# Import app code
|
||||
# Absolute imports for Hydrogen (Jupyter Kernel) compatibility
|
||||
from app.core.config import SENTRY_DSN
|
||||
|
||||
client_sentry = Client(SENTRY_DSN)
|
||||
client_sentry = Client(config.SENTRY_DSN)
|
||||
|
||||
|
||||
@celery_app.task(acks_late=True)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Let the DB start
|
||||
python /app/app/backend_pre_start.py
|
||||
|
||||
|
||||
LOG_LEVEL=info
|
||||
# Uncomment to squeeze performance in exchange of logs
|
||||
# LOG_LEVEL=warning
|
||||
|
||||
# Get CPU cores
|
||||
CORES=$(nproc --all)
|
||||
# Read env var WORKERS_PER_CORE with default of 2
|
||||
WORKERS_PER_CORE_PERCENT=${WORKERS_PER_CORE_PERCENT:-200}
|
||||
# Compute DEFAULT_WEB_CONCURRENCY as CPU cores * workers per core
|
||||
DEFAULT_WEB_CONCURRENCY=$(( ($CORES * $WORKERS_PER_CORE_PERCENT) / 100 ))
|
||||
# Minimum default of workers is 1
|
||||
if [ "$DEFAULT_WEB_CONCURRENCY" -lt 1 ]; then
|
||||
DEFAULT_WEB_CONCURRENCY=1
|
||||
fi
|
||||
# Read WEB_CONCURRENCY env var, with default of computed value
|
||||
WEB_CONCURRENCY=${WEB_CONCURRENCY:-$DEFAULT_WEB_CONCURRENCY}
|
||||
echo "Using these many workers: $WEB_CONCURRENCY"
|
||||
|
||||
gunicorn -k uvicorn.workers.UvicornWorker --log-level $LOG_LEVEL app.main:app --bind 0.0.0.0:80
|
||||
@@ -2,3 +2,9 @@
|
||||
|
||||
# Let the DB start
|
||||
python /app/app/backend_pre_start.py
|
||||
|
||||
# Run migrations
|
||||
alembic upgrade head
|
||||
|
||||
# Create initial data in DB
|
||||
python /app/app/initial_data.py
|
||||
|
||||
@@ -5,3 +5,4 @@ set -x
|
||||
autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py
|
||||
isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app
|
||||
black app
|
||||
vulture app
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM python:3.6
|
||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.6
|
||||
|
||||
RUN pip install celery==4.2.1 passlib[bcrypt] tenacity requests pydantic emails fastapi>=0.2.0 uvicorn gunicorn pyjwt python-multipart email_validator jinja2 psycopg2-binary alembic SQLAlchemy
|
||||
RUN pip install celery==4.2.1 passlib[bcrypt] tenacity requests pydantic emails "fastapi>=0.6.0" uvicorn gunicorn pyjwt python-multipart email_validator jinja2 psycopg2-binary alembic SQLAlchemy
|
||||
|
||||
# For development, Jupyter remote kernel, Hydrogen
|
||||
# Using inside the container:
|
||||
@@ -15,5 +15,3 @@ WORKDIR /app/
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["bash", "/app/backend-start.sh"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM python:3.6
|
||||
|
||||
RUN pip install raven celery==4.2.1 passlib[bcrypt] tenacity requests fastapi>=0.1.13 pydantic emails pyjwt email_validator jinja2 psycopg2-binary alembic SQLAlchemy
|
||||
RUN pip install raven celery==4.2.1 passlib[bcrypt] tenacity requests "fastapi>=0.6.0" emails pyjwt email_validator jinja2 psycopg2-binary alembic SQLAlchemy
|
||||
|
||||
# For development, Jupyter remote kernel, Hydrogen
|
||||
# Using inside the container:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM python:3.6
|
||||
|
||||
RUN pip install requests pytest tenacity passlib[bcrypt] pydantic fastapi>=0.1.13 psycopg2-binary SQLAlchemy
|
||||
RUN pip install requests pytest tenacity passlib[bcrypt] pydantic "fastapi>=0.6.0" psycopg2-binary SQLAlchemy
|
||||
|
||||
# For development, Jupyter remote kernel, Hydrogen
|
||||
# Using inside the container:
|
||||
|
||||
@@ -2,8 +2,8 @@ version: '3.3'
|
||||
services:
|
||||
backend:
|
||||
depends_on:
|
||||
- couchbase
|
||||
- db
|
||||
celeryworker:
|
||||
depends_on:
|
||||
- couchbase
|
||||
- db
|
||||
- queue
|
||||
|
||||
@@ -6,9 +6,8 @@ services:
|
||||
dockerfile: tests.dockerfile
|
||||
command: bash -c "while true; do sleep 1; done"
|
||||
env_file:
|
||||
- env-couchbase.env
|
||||
- env-sync-gateway.env
|
||||
- env-backend.env
|
||||
- env-postgres.env
|
||||
environment:
|
||||
- SERVER_NAME=backend
|
||||
backend:
|
||||
|
||||
@@ -5280,12 +5280,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -5300,17 +5302,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -5427,7 +5432,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -5439,6 +5445,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -5453,6 +5460,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -5460,12 +5468,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -5484,6 +5494,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -5564,7 +5575,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -5576,6 +5588,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -5697,6 +5710,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
||||
@@ -27,17 +27,14 @@ export const api = {
|
||||
async getUsers(token: string) {
|
||||
return axios.get<IUserProfile[]>(`${apiUrl}/api/v1/users/`, authHeaders(token));
|
||||
},
|
||||
async updateUser(token: string, name: string, data: IUserProfileUpdate) {
|
||||
return axios.put(`${apiUrl}/api/v1/users/${name}`, data, authHeaders(token));
|
||||
async updateUser(token: string, userId: number, data: IUserProfileUpdate) {
|
||||
return axios.put(`${apiUrl}/api/v1/users/${userId}`, data, authHeaders(token));
|
||||
},
|
||||
async createUser(token: string, data: IUserProfileCreate) {
|
||||
return axios.post(`${apiUrl}/api/v1/users/`, data, authHeaders(token));
|
||||
},
|
||||
async getRoles(token: string) {
|
||||
return axios.get(`${apiUrl}/api/v1/roles/`, authHeaders(token));
|
||||
},
|
||||
async passwordRecovery(username: string) {
|
||||
return axios.post(`${apiUrl}/api/v1/password-recovery/${username}`);
|
||||
async passwordRecovery(email: string) {
|
||||
return axios.post(`${apiUrl}/api/v1/password-recovery/${email}`);
|
||||
},
|
||||
async resetPassword(password: string, token: string) {
|
||||
return axios.post(`${apiUrl}/api/v1/reset-password/`, {
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
export interface IUserProfile {
|
||||
admin_channels: string[];
|
||||
admin_roles: string[];
|
||||
disabled: boolean;
|
||||
email: string;
|
||||
human_name: string;
|
||||
name: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
full_name: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface IUserProfileUpdate {
|
||||
human_name?: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
admin_channels?: string[];
|
||||
admin_roles?: string[];
|
||||
disabled?: boolean;
|
||||
full_name?: string;
|
||||
password?: string;
|
||||
is_active?: boolean;
|
||||
is_superuser?: boolean;
|
||||
}
|
||||
|
||||
export interface IUserProfileCreate {
|
||||
name: string;
|
||||
human_name?: string;
|
||||
email: string;
|
||||
full_name?: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
admin_channels?: string[];
|
||||
admin_roles?: string[];
|
||||
disabled?: boolean;
|
||||
is_active?: boolean;
|
||||
is_superuser?: boolean;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export default new Router({
|
||||
/* webpackChunkName: "main-admin-users" */ './views/main/admin/AdminUsers.vue'),
|
||||
},
|
||||
{
|
||||
path: 'users/edit/:name',
|
||||
path: 'users/edit/:id',
|
||||
name: 'main-admin-users-edit',
|
||||
component: () => import(
|
||||
/* webpackChunkName: "main-admin-users-edit" */ './views/main/admin/EditUser.vue'),
|
||||
|
||||
@@ -5,6 +5,5 @@ import { AdminState } from '../state';
|
||||
|
||||
const {commit} = getStoreAccessors<AdminState, State>('');
|
||||
|
||||
export const commitSetRoles = commit(mutations.setRoles);
|
||||
export const commitSetUser = commit(mutations.setUser);
|
||||
export const commitSetUsers = commit(mutations.setUsers);
|
||||
|
||||
@@ -6,6 +6,5 @@ import { actions } from '../actions';
|
||||
const {dispatch} = getStoreAccessors<AdminState, State>('');
|
||||
|
||||
export const dispatchCreateUser = dispatch(actions.actionCreateUser);
|
||||
export const dispatchGetRoles = dispatch(actions.actionGetRoles);
|
||||
export const dispatchGetUsers = dispatch(actions.actionGetUsers);
|
||||
export const dispatchUpdateUser = dispatch(actions.actionUpdateUser);
|
||||
|
||||
@@ -6,5 +6,4 @@ import { getters } from '../getters';
|
||||
const { read } = getStoreAccessors<AdminState, State>('');
|
||||
|
||||
export const readAdminOneUser = read(getters.adminOneUser);
|
||||
export const readAdminRoles = read(getters.adminRoles);
|
||||
export const readAdminUsers = read(getters.adminUsers);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ActionContext } from 'vuex';
|
||||
import {
|
||||
commitSetUsers,
|
||||
commitSetUser,
|
||||
commitSetRoles,
|
||||
} from './accessors/commit';
|
||||
import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces';
|
||||
import { State } from '../state';
|
||||
@@ -23,12 +22,12 @@ export const actions = {
|
||||
await dispatchCheckApiError(context, error);
|
||||
}
|
||||
},
|
||||
async actionUpdateUser(context: MainContext, payload: { name: string, user: IUserProfileUpdate }) {
|
||||
async actionUpdateUser(context: MainContext, payload: { id: number, user: IUserProfileUpdate }) {
|
||||
try {
|
||||
const loadingNotification = { content: 'saving', showProgress: true };
|
||||
commitAddNotification(context, loadingNotification);
|
||||
const response = (await Promise.all([
|
||||
api.updateUser(context.rootState.main.token, payload.name, payload.user),
|
||||
api.updateUser(context.rootState.main.token, payload.id, payload.user),
|
||||
await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
|
||||
]))[0];
|
||||
commitSetUser(context, response.data);
|
||||
@@ -53,12 +52,4 @@ export const actions = {
|
||||
await dispatchCheckApiError(context, error);
|
||||
}
|
||||
},
|
||||
async actionGetRoles(context: MainContext) {
|
||||
try {
|
||||
const response = await api.getRoles(context.rootState.main.token);
|
||||
commitSetRoles(context, response.data.roles);
|
||||
} catch (error) {
|
||||
await dispatchCheckApiError(context, error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,9 +2,8 @@ import { AdminState } from './state';
|
||||
|
||||
export const getters = {
|
||||
adminUsers: (state: AdminState) => state.users,
|
||||
adminRoles: (state: AdminState) => state.roles,
|
||||
adminOneUser: (state: AdminState) => (name: string) => {
|
||||
const filteredUsers = state.users.filter((user) => user.name === name);
|
||||
adminOneUser: (state: AdminState) => (userId: number) => {
|
||||
const filteredUsers = state.users.filter((user) => user.id === userId);
|
||||
if (filteredUsers.length > 0) {
|
||||
return { ...filteredUsers[0] };
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { AdminState } from './state';
|
||||
|
||||
const defaultState: AdminState = {
|
||||
users: [],
|
||||
roles: [],
|
||||
};
|
||||
|
||||
export const adminModule = {
|
||||
|
||||
@@ -6,11 +6,8 @@ export const mutations = {
|
||||
state.users = payload;
|
||||
},
|
||||
setUser(state: AdminState, payload: IUserProfile) {
|
||||
const users = state.users.filter((user: IUserProfile) => user.name !== payload.name);
|
||||
const users = state.users.filter((user: IUserProfile) => user.id !== payload.id);
|
||||
users.push(payload);
|
||||
state.users = users;
|
||||
},
|
||||
setRoles(state: AdminState, payload: string[]) {
|
||||
state.roles = payload;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,5 +2,4 @@ import { IUserProfile } from '@/interfaces';
|
||||
|
||||
export interface AdminState {
|
||||
users: IUserProfile[];
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
commitAddNotification,
|
||||
} from './accessors';
|
||||
import { AxiosError } from 'axios';
|
||||
import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces';
|
||||
import { State } from '../state';
|
||||
import { MainState, AppNotification } from './state';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export const getters = {
|
||||
hasAdminAccess: (state: MainState) => {
|
||||
return (
|
||||
state.userProfile &&
|
||||
state.userProfile.admin_roles.includes('superuser'));
|
||||
state.userProfile.is_superuser && state.userProfile.is_active);
|
||||
},
|
||||
loginError: (state: MainState) => state.logInError,
|
||||
dashboardShowDrawer: (state: MainState) => state.dashboardShowDrawer,
|
||||
|
||||
@@ -23,11 +23,11 @@ import { readUserProfile } from '@/store/main/accessors';
|
||||
export default class Dashboard extends Vue {
|
||||
get greetedUser() {
|
||||
const userProfile = readUserProfile(this.$store);
|
||||
if (userProfile && userProfile.human_name) {
|
||||
if (userProfile.human_name) {
|
||||
return userProfile.human_name;
|
||||
if (userProfile && userProfile.full_name) {
|
||||
if (userProfile.full_name) {
|
||||
return userProfile.full_name;
|
||||
} else {
|
||||
return userProfile.name;
|
||||
return userProfile.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { store } from '@/store';
|
||||
import { readHasAdminAccess } from '@/store/main/accessors';
|
||||
import { dispatchGetRoles } from '@/store/admin/accessors';
|
||||
|
||||
const routeGuardAdmin = async (to, from, next) => {
|
||||
if (!readHasAdminAccess(store)) {
|
||||
@@ -17,7 +16,7 @@ const routeGuardAdmin = async (to, from, next) => {
|
||||
};
|
||||
|
||||
@Component
|
||||
export default class Start extends Vue {
|
||||
export default class Admin extends Vue {
|
||||
public beforeRouteEnter(to, from, next) {
|
||||
routeGuardAdmin(to, from, next);
|
||||
}
|
||||
@@ -25,9 +24,5 @@ export default class Start extends Vue {
|
||||
public beforeRouteUpdate(to, from, next) {
|
||||
routeGuardAdmin(to, from, next);
|
||||
}
|
||||
|
||||
public async mounted() {
|
||||
await dispatchGetRoles(this.$store);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,15 +11,13 @@
|
||||
<template slot="items" slot-scope="props">
|
||||
<td>{{ props.item.name }}</td>
|
||||
<td>{{ props.item.email }}</td>
|
||||
<td>{{ props.item.human_name }}</td>
|
||||
<td>{{ props.item.disabled }}</td>
|
||||
<td>
|
||||
<v-chip v-for="role in props.item.admin_roles" :key="role">{{role}}</v-chip>
|
||||
</td>
|
||||
<td>{{ props.item.full_name }}</td>
|
||||
<td><v-icon v-if="props.item.is_active">checkmark</v-icon></td>
|
||||
<td><v-icon v-if="props.item.is_superuser">checkmark</v-icon></td>
|
||||
<td class="justify-center layout px-0">
|
||||
<v-tooltip top>
|
||||
<span>Edit</span>
|
||||
<v-btn slot="activator" flat :to="{name: 'main-admin-users-edit', params: {name: props.item.name}}">
|
||||
<v-btn slot="activator" flat :to="{name: 'main-admin-users-edit', params: {id: props.item.id}}">
|
||||
<v-icon>edit</v-icon>
|
||||
</v-btn>
|
||||
</v-tooltip>
|
||||
@@ -36,7 +34,7 @@ import { IUserProfile } from '@/interfaces';
|
||||
import { readAdminUsers, dispatchGetUsers } from '@/store/admin/accessors';
|
||||
|
||||
@Component
|
||||
export default class UserProfile extends Vue {
|
||||
export default class AdminUsers extends Vue {
|
||||
public headers = [
|
||||
{
|
||||
text: 'Name',
|
||||
@@ -53,23 +51,24 @@ export default class UserProfile extends Vue {
|
||||
{
|
||||
text: 'Full Name',
|
||||
sortable: true,
|
||||
value: 'human_name',
|
||||
value: 'full_name',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
text: 'Disabled',
|
||||
text: 'Is Active',
|
||||
sortable: true,
|
||||
value: 'disabled',
|
||||
value: 'isActive',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
text: 'Roles',
|
||||
value: 'admin_roles',
|
||||
text: 'Is Superuser',
|
||||
sortable: true,
|
||||
value: 'isSuperuser',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
text: 'Actions',
|
||||
value: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
];
|
||||
get users() {
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
<v-card-text>
|
||||
<template>
|
||||
<v-form v-model="valid" ref="form" lazy-validation>
|
||||
<v-text-field label="Username" v-model="name" required></v-text-field>
|
||||
<v-text-field label="Full Name" v-model="fullName" required></v-text-field>
|
||||
<v-text-field label="E-mail" type="email" v-model="email" v-validate="'required|email'" data-vv-name="email" :error-messages="errors.collect('email')" required></v-text-field>
|
||||
<div class="subheading secondary--text text--lighten-2">Roles</div>
|
||||
<v-checkbox v-for="(value, role) in selectedRoles" :key="role" :label="role" v-model="selectedRoles[role]"></v-checkbox>
|
||||
<div class="subheading secondary--text text--lighten-2">Disable User <span v-if="userDisabled">(currently disabled)</span><span v-else>(currently enabled)</span></div>
|
||||
<v-checkbox :label="'Disabled'" v-model="userDisabled"></v-checkbox>
|
||||
<div class="subheading secondary--text text--lighten-2">User is superuser <span v-if="isSuperuser">(currently is a superuser)</span><span v-else>(currently is not a superuser)</span></div>
|
||||
<v-checkbox label="Is Superuser" v-model="isSuperuser"></v-checkbox>
|
||||
<div class="subheading secondary--text text--lighten-2">User is active <span v-if="isActive">(currently active)</span><span v-else>(currently not active)</span></div>
|
||||
<v-checkbox label="Is Active" v-model="isActive"></v-checkbox>
|
||||
<v-layout align-center>
|
||||
<v-flex>
|
||||
<v-text-field type="password" ref="password" label="Set Password" data-vv-name="password" data-vv-delay="100" v-validate="{required: true}" v-model="password1" :error-messages="errors.first('password')">
|
||||
@@ -41,38 +40,32 @@ import {
|
||||
IUserProfileUpdate,
|
||||
IUserProfileCreate,
|
||||
} from '@/interfaces';
|
||||
import { dispatchGetUsers, dispatchGetRoles, dispatchCreateUser, readAdminRoles } from '@/store/admin/accessors';
|
||||
import { dispatchGetUsers, dispatchCreateUser } from '@/store/admin/accessors';
|
||||
|
||||
@Component
|
||||
export default class EditUser extends Vue {
|
||||
export default class CreateUser extends Vue {
|
||||
public valid = false;
|
||||
public name: string = '';
|
||||
public fullName: string = '';
|
||||
public email: string = '';
|
||||
public isActive: boolean = true;
|
||||
public isSuperuser: boolean = false;
|
||||
public setPassword = false;
|
||||
public password1: string = '';
|
||||
public password2: string = '';
|
||||
public userDisabled: boolean = false;
|
||||
|
||||
public selectedRoles: { [role: string]: boolean } = {};
|
||||
|
||||
public async mounted() {
|
||||
await dispatchGetUsers(this.$store);
|
||||
await dispatchGetRoles(this.$store);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.password1 = '';
|
||||
this.password2 = '';
|
||||
this.name = '';
|
||||
this.fullName = '';
|
||||
this.email = '';
|
||||
this.userDisabled = false;
|
||||
this.isActive = true;
|
||||
this.isSuperuser = false;
|
||||
this.$validator.reset();
|
||||
this.availableRoles.forEach((value) => {
|
||||
Vue.set(this.selectedRoles, value, false);
|
||||
});
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
@@ -82,29 +75,20 @@ export default class EditUser extends Vue {
|
||||
public async submit() {
|
||||
if (await this.$validator.validateAll()) {
|
||||
const updatedProfile: IUserProfileCreate = {
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
};
|
||||
if (this.fullName) {
|
||||
updatedProfile.human_name = this.fullName;
|
||||
updatedProfile.full_name = this.fullName;
|
||||
}
|
||||
if (this.email) {
|
||||
updatedProfile.email = this.email;
|
||||
}
|
||||
updatedProfile.disabled = this.userDisabled;
|
||||
updatedProfile.admin_roles = [];
|
||||
this.availableRoles.forEach((role: string) => {
|
||||
if (this.selectedRoles[role]) {
|
||||
updatedProfile.admin_roles!.push(role);
|
||||
}
|
||||
});
|
||||
updatedProfile.is_active = this.isActive;
|
||||
updatedProfile.is_superuser = this.isSuperuser;
|
||||
updatedProfile.password = this.password1;
|
||||
await dispatchCreateUser(this.$store, updatedProfile);
|
||||
this.$router.push('/main/admin/users');
|
||||
}
|
||||
}
|
||||
|
||||
get availableRoles() {
|
||||
return readAdminRoles(this.$store);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
<template>
|
||||
<div class="my-3">
|
||||
<div class="subheading secondary--text text--lighten-2">Username</div>
|
||||
<div class="title primary--text text--darken-2" v-if="user">{{user.name}}</div>
|
||||
<div class="title primary--text text--darken-2" v-if="user">{{user.email}}</div>
|
||||
<div class="title primary--text text--darken-2" v-else>-----</div>
|
||||
</div>
|
||||
<v-form v-model="valid" ref="form" lazy-validation>
|
||||
<v-text-field label="Full Name" v-model="fullName" required></v-text-field>
|
||||
<v-text-field label="E-mail" type="email" v-model="email" v-validate="'required|email'" data-vv-name="email" :error-messages="errors.collect('email')" required></v-text-field>
|
||||
<div class="subheading secondary--text text--lighten-2">Roles</div>
|
||||
<v-checkbox v-for="(value, role) in selectedRoles" :key="role" :label="role" v-model="selectedRoles[role]"></v-checkbox>
|
||||
<div class="subheading secondary--text text--lighten-2">Disable User <span v-if="userDisabled">(currently disabled)</span><span v-else>(currently enabled)</span></div>
|
||||
<v-checkbox :label="'Disabled'" v-model="userDisabled"></v-checkbox>
|
||||
<div class="subheading secondary--text text--lighten-2">User is superuser <span v-if="isSuperuser">(currently is a superuser)</span><span v-else>(currently is not a superuser)</span></div>
|
||||
<v-checkbox label="Is Superuser" v-model="isSuperuser"></v-checkbox>
|
||||
<div class="subheading secondary--text text--lighten-2">User is active <span v-if="isActive">(currently active)</span><span v-else>(currently not active)</span></div>
|
||||
<v-checkbox label="Is Active" v-model="isActive"></v-checkbox>
|
||||
<v-layout align-center>
|
||||
<v-flex shrink>
|
||||
<v-checkbox v-model="setPassword" class="mr-2"></v-checkbox>
|
||||
@@ -48,31 +48,23 @@ import { Component, Vue } from 'vue-property-decorator';
|
||||
import { IUserProfile, IUserProfileUpdate } from '@/interfaces';
|
||||
import {
|
||||
dispatchGetUsers,
|
||||
dispatchGetRoles,
|
||||
dispatchUpdateUser,
|
||||
readAdminOneUser,
|
||||
readAdminRoles,
|
||||
} from '@/store/admin/accessors';
|
||||
|
||||
@Component
|
||||
export default class EditUser extends Vue {
|
||||
public valid = true;
|
||||
public name: string = '';
|
||||
public fullName: string = '';
|
||||
public email: string = '';
|
||||
public isActive: boolean = true;
|
||||
public isSuperuser: boolean = false;
|
||||
public setPassword = false;
|
||||
public password1: string = '';
|
||||
public password2: string = '';
|
||||
public userDisabled: boolean = false;
|
||||
|
||||
public selectedRoles: { [role: string]: boolean } = {};
|
||||
|
||||
public async mounted() {
|
||||
await dispatchGetUsers(this.$store);
|
||||
await dispatchGetRoles(this.$store);
|
||||
this.availableRoles.forEach((value) => {
|
||||
Vue.set(this.selectedRoles, value, false);
|
||||
});
|
||||
this.reset();
|
||||
}
|
||||
|
||||
@@ -82,17 +74,10 @@ export default class EditUser extends Vue {
|
||||
this.password2 = '';
|
||||
this.$validator.reset();
|
||||
if (this.user) {
|
||||
this.name = this.user.name;
|
||||
this.fullName = this.user.human_name;
|
||||
this.fullName = this.user.full_name;
|
||||
this.email = this.user.email;
|
||||
this.userDisabled = this.user.disabled;
|
||||
this.availableRoles.forEach((role: string) => {
|
||||
if (this.user!.admin_roles.includes(role)) {
|
||||
Vue.set(this.selectedRoles, role, true);
|
||||
} else {
|
||||
Vue.set(this.selectedRoles, role, false);
|
||||
}
|
||||
});
|
||||
this.isActive = this.user.is_active;
|
||||
this.isSuperuser = this.user.is_superuser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,33 +89,23 @@ export default class EditUser extends Vue {
|
||||
if (await this.$validator.validateAll()) {
|
||||
const updatedProfile: IUserProfileUpdate = {};
|
||||
if (this.fullName) {
|
||||
updatedProfile.human_name = this.fullName;
|
||||
updatedProfile.full_name = this.fullName;
|
||||
}
|
||||
if (this.email) {
|
||||
updatedProfile.email = this.email;
|
||||
}
|
||||
updatedProfile.disabled = this.userDisabled;
|
||||
updatedProfile.admin_roles = [];
|
||||
this.availableRoles.forEach((role: string) => {
|
||||
if (this.selectedRoles[role]) {
|
||||
updatedProfile.admin_roles!.push(role);
|
||||
}
|
||||
});
|
||||
updatedProfile.is_active = this.isActive;
|
||||
updatedProfile.is_superuser = this.isSuperuser;
|
||||
if (this.setPassword) {
|
||||
updatedProfile.password = this.password1;
|
||||
}
|
||||
const payload = { name: this.name, user: updatedProfile };
|
||||
await dispatchUpdateUser(this.$store, payload);
|
||||
await dispatchUpdateUser(this.$store, { id: this.user!.id, user: updatedProfile });
|
||||
this.$router.push('/main/admin/users');
|
||||
}
|
||||
}
|
||||
|
||||
get user() {
|
||||
return readAdminOneUser(this.$store)(this.$router.currentRoute.params.name);
|
||||
}
|
||||
|
||||
get availableRoles() {
|
||||
return readAdminRoles(this.$store);
|
||||
return readAdminOneUser(this.$store)(+this.$router.currentRoute.params.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,12 +7,7 @@
|
||||
<v-card-text>
|
||||
<div class="my-4">
|
||||
<div class="subheading secondary--text text--lighten-3">Full Name</div>
|
||||
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.human_name">{{userProfile.human_name}}</div>
|
||||
<div class="title primary--text text--darken-2" v-else>-----</div>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<div class="subheading secondary--text text--lighten-3">Username</div>
|
||||
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.name">{{userProfile.name}}</div>
|
||||
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.full_name">{{userProfile.full_name}}</div>
|
||||
<div class="title primary--text text--darken-2" v-else>-----</div>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<template>
|
||||
<div class="my-3">
|
||||
<div class="subheading secondary--text text--lighten-2">Username</div>
|
||||
<div class="title primary--text text--darken-2" v-if="userProfile.name">{{userProfile.name}}</div>
|
||||
<div class="title primary--text text--darken-2" v-else>-----</div>
|
||||
</div>
|
||||
<v-form v-model="valid" ref="form" lazy-validation>
|
||||
<v-text-field label="Full Name" v-model="fullName" required></v-text-field>
|
||||
<v-text-field label="E-mail" type="email" v-model="email" v-validate="'required|email'" data-vv-name="email" :error-messages="errors.collect('email')" required></v-text-field>
|
||||
@@ -41,7 +36,7 @@ export default class UserProfileEdit extends Vue {
|
||||
public created() {
|
||||
const userProfile = readUserProfile(this.$store);
|
||||
if (userProfile) {
|
||||
this.fullName = userProfile.human_name;
|
||||
this.fullName = userProfile.full_name;
|
||||
this.email = userProfile.email;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +48,7 @@ export default class UserProfileEdit extends Vue {
|
||||
public reset() {
|
||||
const userProfile = readUserProfile(this.$store);
|
||||
if (userProfile) {
|
||||
this.fullName = userProfile.human_name;
|
||||
this.fullName = userProfile.full_name;
|
||||
this.email = userProfile.email;
|
||||
}
|
||||
}
|
||||
@@ -66,7 +61,7 @@ export default class UserProfileEdit extends Vue {
|
||||
if ((this.$refs.form as any).validate()) {
|
||||
const updatedProfile: IUserProfileUpdate = {};
|
||||
if (this.fullName) {
|
||||
updatedProfile.human_name = this.fullName;
|
||||
updatedProfile.full_name = this.fullName;
|
||||
}
|
||||
if (this.email) {
|
||||
updatedProfile.email = this.email;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<v-card-text>
|
||||
<template>
|
||||
<div class="my-3">
|
||||
<div class="subheading secondary--text text--lighten-2">Username</div>
|
||||
<div class="title primary--text text--darken-2" v-if="userProfile.name">{{userProfile.name}}</div>
|
||||
<div class="title primary--text text--darken-2" v-else>-----</div>
|
||||
<div class="subheading secondary--text text--lighten-2">User</div>
|
||||
<div class="title primary--text text--darken-2" v-if="userProfile.full_name">{{userProfile.full_name}}</div>
|
||||
<div class="title primary--text text--darken-2" v-else>{{userProfile.email}}</div>
|
||||
</div>
|
||||
<v-form ref="form">
|
||||
<v-text-field
|
||||
|
||||
Reference in New Issue
Block a user