diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index a2a09ac..5fdc479 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/Pipfile b/{{cookiecutter.project_slug}}/backend/app/Pipfile index 85efdfd..82bd203 100644 --- a/{{cookiecutter.project_slug}}/backend/app/Pipfile +++ b/{{cookiecutter.project_slug}}/backend/app/Pipfile @@ -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" diff --git a/{{cookiecutter.project_slug}}/backend/app/Pipfile.lock b/{{cookiecutter.project_slug}}/backend/app/Pipfile.lock index 9f8798e..3358988 100644 --- a/{{cookiecutter.project_slug}}/backend/app/Pipfile.lock +++ b/{{cookiecutter.project_slug}}/backend/app/Pipfile.lock @@ -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", diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic.ini b/{{cookiecutter.project_slug}}/backend/app/alembic.ini new file mode 100755 index 0000000..09ce6c4 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/alembic.ini @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic/versions/e6ae69e9dcb9_first_revision.py b/{{cookiecutter.project_slug}}/backend/app/alembic/versions/e6ae69e9dcb9_first_revision.py new file mode 100644 index 0000000..6034264 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/alembic/versions/e6ae69e9dcb9_first_revision.py @@ -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 ### diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py index 5ec46a5..81b645d 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py deleted file mode 100644 index 18118b1..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py +++ /dev/null @@ -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} diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py index 26a1725..402005a 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py @@ -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"} diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py index 2af90ba..31668dc 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py index 34a869b..874ef5b 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py @@ -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"} diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py new file mode 100644 index 0000000..24a437e --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py @@ -0,0 +1,5 @@ +from starlette.requests import Request + + +def get_db(request: Request): + return request.state.db diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py new file mode 100644 index 0000000..e8d16c3 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py index 932ed41..41b6b63 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py @@ -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__) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py index 932ed41..41b6b63 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py @@ -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__) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py index b1568ee..07e42b0 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py @@ -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") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py b/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py index 0acd756..4dc2835 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py index df7b656..8d2a49f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py index 531a0bb..938aa6c 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py deleted file mode 100644 index fde3c59..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py +++ /dev/null @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/base.py b/{{cookiecutter.project_slug}}/backend/app/app/db/base.py index 62920c7..44a65b5 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/base.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/base.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/full_text_search_utils.py b/{{cookiecutter.project_slug}}/backend/app/app/db/full_text_search_utils.py deleted file mode 100644 index f8d45fa..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/full_text_search_utils.py +++ /dev/null @@ -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" diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py index 1a80545..26e9039 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/external_session.py b/{{cookiecutter.project_slug}}/backend/app/app/db/session.py similarity index 80% rename from {{cookiecutter.project_slug}}/backend/app/app/db/external_session.py rename to {{cookiecutter.project_slug}}/backend/app/app/db/session.py index 82a1a4b..352738e 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/external_session.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/session.py @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db_models/base_relations.py b/{{cookiecutter.project_slug}}/backend/app/app/db_models/base_relations.py deleted file mode 100755 index 9b7309d..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/db_models/base_relations.py +++ /dev/null @@ -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")), -) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db_models/role.py b/{{cookiecutter.project_slug}}/backend/app/app/db_models/role.py deleted file mode 100755 index b0e18d8..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/db_models/role.py +++ /dev/null @@ -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") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db_models/user.py b/{{cookiecutter.project_slug}}/backend/app/app/db_models/user.py index 5ad542a..cdfffaa 100755 --- a/{{cookiecutter.project_slug}}/backend/app/app/db_models/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db_models/user.py @@ -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] diff --git a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py new file mode 100644 index 0000000..a572ada --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py @@ -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() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/main.py b/{{cookiecutter.project_slug}}/backend/app/app/main.py index 328dabd..a02a6f7 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/main.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/main.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/config.py b/{{cookiecutter.project_slug}}/backend/app/app/models/config.py deleted file mode 100644 index 7c55d2c..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/config.py +++ /dev/null @@ -1 +0,0 @@ -USERPROFILE_DOC_TYPE = "userprofile" diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/role.py b/{{cookiecutter.project_slug}}/backend/app/app/models/role.py deleted file mode 100644 index 789013e..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/role.py +++ /dev/null @@ -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] diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/token.py b/{{cookiecutter.project_slug}}/backend/app/app/models/token.py index 764765f..75c0f4a 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/token.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/models/token.py @@ -7,4 +7,4 @@ class Token(BaseModel): class TokenPayload(BaseModel): - username: str = None + user_id: int = None diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py b/{{cookiecutter.project_slug}}/backend/app/app/models/user.py index 0b5ab4c..54636e1 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/models/user.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users.json b/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users.json deleted file mode 100644 index 55c9a40..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "users", - "type": "fulltext-alias", - "params": { - "targets": { - "users_01": {} - } - }, - "sourceType": "nil", - "sourceName": "", - "sourceUUID": "", - "sourceParams": null, - "planParams": {}, - "uuid": "" -} diff --git a/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users_01.json b/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users_01.json deleted file mode 100644 index e297177..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users_01.json +++ /dev/null @@ -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": "" -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_token.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_token.py index 08ba2eb..015ec00 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_token.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_token.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py index 94ae472..d2ba9fd 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_get_ids.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_get_ids.py deleted file mode 100644 index 038e838..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_get_ids.py +++ /dev/null @@ -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" diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py index 27c0e1b..b70e0a8 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py index 476659f..6618668 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py @@ -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__) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/utils.py index b9347cb..0518912 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/utils.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/utils.py @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/worker.py b/{{cookiecutter.project_slug}}/backend/app/app/worker.py index 89a2a06..2a4a089 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/worker.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/worker.py @@ -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) diff --git a/{{cookiecutter.project_slug}}/backend/app/backend-start.sh b/{{cookiecutter.project_slug}}/backend/app/backend-start.sh deleted file mode 100644 index 7a7893f..0000000 --- a/{{cookiecutter.project_slug}}/backend/app/backend-start.sh +++ /dev/null @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/prestart.sh b/{{cookiecutter.project_slug}}/backend/app/prestart.sh index f95ea91..fc1e5f1 100644 --- a/{{cookiecutter.project_slug}}/backend/app/prestart.sh +++ b/{{cookiecutter.project_slug}}/backend/app/prestart.sh @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh index a7c6da9..08bb841 100644 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh @@ -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 diff --git a/{{cookiecutter.project_slug}}/backend/backend.dockerfile b/{{cookiecutter.project_slug}}/backend/backend.dockerfile index b59694f..1d102cd 100644 --- a/{{cookiecutter.project_slug}}/backend/backend.dockerfile +++ b/{{cookiecutter.project_slug}}/backend/backend.dockerfile @@ -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"] diff --git a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile index cd9be2d..af29ac4 100644 --- a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile +++ b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile @@ -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: diff --git a/{{cookiecutter.project_slug}}/backend/tests.dockerfile b/{{cookiecutter.project_slug}}/backend/tests.dockerfile index 3a0c7ee..d2b849f 100644 --- a/{{cookiecutter.project_slug}}/backend/tests.dockerfile +++ b/{{cookiecutter.project_slug}}/backend/tests.dockerfile @@ -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: diff --git a/{{cookiecutter.project_slug}}/docker-compose.shared.depends.yml b/{{cookiecutter.project_slug}}/docker-compose.shared.depends.yml index a3a6977..2a52bde 100644 --- a/{{cookiecutter.project_slug}}/docker-compose.shared.depends.yml +++ b/{{cookiecutter.project_slug}}/docker-compose.shared.depends.yml @@ -2,8 +2,8 @@ version: '3.3' services: backend: depends_on: - - couchbase + - db celeryworker: depends_on: - - couchbase + - db - queue diff --git a/{{cookiecutter.project_slug}}/docker-compose.test.yml b/{{cookiecutter.project_slug}}/docker-compose.test.yml index 69f7c6c..d4cb4ee 100644 --- a/{{cookiecutter.project_slug}}/docker-compose.test.yml +++ b/{{cookiecutter.project_slug}}/docker-compose.test.yml @@ -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: diff --git a/{{cookiecutter.project_slug}}/frontend/package-lock.json b/{{cookiecutter.project_slug}}/frontend/package-lock.json index a5ae7db..b1bd42c 100644 --- a/{{cookiecutter.project_slug}}/frontend/package-lock.json +++ b/{{cookiecutter.project_slug}}/frontend/package-lock.json @@ -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", diff --git a/{{cookiecutter.project_slug}}/frontend/src/api.ts b/{{cookiecutter.project_slug}}/frontend/src/api.ts index 9899070..c24712b 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/api.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/api.ts @@ -27,17 +27,14 @@ export const api = { async getUsers(token: string) { return axios.get(`${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/`, { diff --git a/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts b/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts index 321835e..a1b9340 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts @@ -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; } diff --git a/{{cookiecutter.project_slug}}/frontend/src/router.ts b/{{cookiecutter.project_slug}}/frontend/src/router.ts index 87db7a9..b649c17 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/router.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/router.ts @@ -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'), diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/commit.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/commit.ts index c8c5e85..fd45269 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/commit.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/commit.ts @@ -5,6 +5,5 @@ import { AdminState } from '../state'; const {commit} = getStoreAccessors(''); -export const commitSetRoles = commit(mutations.setRoles); export const commitSetUser = commit(mutations.setUser); export const commitSetUsers = commit(mutations.setUsers); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/dispatch.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/dispatch.ts index bd677cd..cb61223 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/dispatch.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/dispatch.ts @@ -6,6 +6,5 @@ import { actions } from '../actions'; const {dispatch} = getStoreAccessors(''); export const dispatchCreateUser = dispatch(actions.actionCreateUser); -export const dispatchGetRoles = dispatch(actions.actionGetRoles); export const dispatchGetUsers = dispatch(actions.actionGetUsers); export const dispatchUpdateUser = dispatch(actions.actionUpdateUser); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/read.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/read.ts index e46b843..d23894f 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/read.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/read.ts @@ -6,5 +6,4 @@ import { getters } from '../getters'; const { read } = getStoreAccessors(''); export const readAdminOneUser = read(getters.adminOneUser); -export const readAdminRoles = read(getters.adminRoles); export const readAdminUsers = read(getters.adminUsers); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts index 141ae1d..36386a9 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts @@ -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); - } - }, }; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts index 86c1c6b..5b93050 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts @@ -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] }; } diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts index fb29a1c..dcaf6ab 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts @@ -5,7 +5,6 @@ import { AdminState } from './state'; const defaultState: AdminState = { users: [], - roles: [], }; export const adminModule = { diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts index 3b0e846..9801402 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts @@ -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; - }, }; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts index fc4ac8f..8dfefe2 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts @@ -2,5 +2,4 @@ import { IUserProfile } from '@/interfaces'; export interface AdminState { users: IUserProfile[]; - roles: string[]; } diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts index 81308e2..eb9f288 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts @@ -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'; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts index ec61494..c9bc37c 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts @@ -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, diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue index 22e1606..50e107b 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue @@ -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; } } } diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue index f661535..d305831 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue @@ -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); - } } diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue index e4b5f9b..5c47aaa 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue @@ -11,15 +11,13 @@