Browse Source

Update all for Postgres and new techniques

pull/13907/head
Sebastián Ramírez 7 years ago
parent
commit
6fdba19639
  1. 2
      {{cookiecutter.project_slug}}/README.md
  2. 6
      {{cookiecutter.project_slug}}/backend/app/Pipfile
  3. 326
      {{cookiecutter.project_slug}}/backend/app/Pipfile.lock
  4. 74
      {{cookiecutter.project_slug}}/backend/app/alembic.ini
  5. 42
      {{cookiecutter.project_slug}}/backend/app/alembic/versions/e6ae69e9dcb9_first_revision.py
  6. 2
      {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py
  7. 25
      {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py
  8. 64
      {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py
  9. 145
      {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py
  10. 17
      {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py
  11. 0
      {{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py
  12. 5
      {{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py
  13. 30
      {{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py
  14. 2
      {{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py
  15. 2
      {{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py
  16. 4
      {{cookiecutter.project_slug}}/backend/app/app/core/config.py
  17. 29
      {{cookiecutter.project_slug}}/backend/app/app/core/jwt.py
  18. 4
      {{cookiecutter.project_slug}}/backend/app/app/core/security.py
  19. 98
      {{cookiecutter.project_slug}}/backend/app/app/crud/user.py
  20. 214
      {{cookiecutter.project_slug}}/backend/app/app/crud/utils.py
  21. 3
      {{cookiecutter.project_slug}}/backend/app/app/db/base.py
  22. 106
      {{cookiecutter.project_slug}}/backend/app/app/db/full_text_search_utils.py
  23. 24
      {{cookiecutter.project_slug}}/backend/app/app/db/init_db.py
  24. 4
      {{cookiecutter.project_slug}}/backend/app/app/db/session.py
  25. 11
      {{cookiecutter.project_slug}}/backend/app/app/db_models/base_relations.py
  26. 19
      {{cookiecutter.project_slug}}/backend/app/app/db_models/role.py
  27. 23
      {{cookiecutter.project_slug}}/backend/app/app/db_models/user.py
  28. 21
      {{cookiecutter.project_slug}}/backend/app/app/initial_data.py
  29. 20
      {{cookiecutter.project_slug}}/backend/app/app/main.py
  30. 1
      {{cookiecutter.project_slug}}/backend/app/app/models/config.py
  31. 14
      {{cookiecutter.project_slug}}/backend/app/app/models/role.py
  32. 2
      {{cookiecutter.project_slug}}/backend/app/app/models/token.py
  33. 24
      {{cookiecutter.project_slug}}/backend/app/app/models/user.py
  34. 15
      {{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users.json
  35. 157
      {{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users_01.json
  36. 2
      {{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_token.py
  37. 55
      {{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py
  38. 7
      {{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_get_ids.py
  39. 86
      {{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py
  40. 4
      {{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py
  41. 87
      {{cookiecutter.project_slug}}/backend/app/app/utils.py
  42. 11
      {{cookiecutter.project_slug}}/backend/app/app/worker.py
  43. 27
      {{cookiecutter.project_slug}}/backend/app/backend-start.sh
  44. 6
      {{cookiecutter.project_slug}}/backend/app/prestart.sh
  45. 1
      {{cookiecutter.project_slug}}/backend/app/scripts/lint.sh
  46. 6
      {{cookiecutter.project_slug}}/backend/backend.dockerfile
  47. 2
      {{cookiecutter.project_slug}}/backend/celeryworker.dockerfile
  48. 2
      {{cookiecutter.project_slug}}/backend/tests.dockerfile
  49. 4
      {{cookiecutter.project_slug}}/docker-compose.shared.depends.yml
  50. 3
      {{cookiecutter.project_slug}}/docker-compose.test.yml
  51. 28
      {{cookiecutter.project_slug}}/frontend/package-lock.json
  52. 11
      {{cookiecutter.project_slug}}/frontend/src/api.ts
  53. 28
      {{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts
  54. 2
      {{cookiecutter.project_slug}}/frontend/src/router.ts
  55. 1
      {{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/commit.ts
  56. 1
      {{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/dispatch.ts
  57. 1
      {{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/read.ts
  58. 13
      {{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts
  59. 5
      {{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts
  60. 1
      {{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts
  61. 5
      {{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts
  62. 1
      {{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts
  63. 1
      {{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts
  64. 2
      {{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts
  65. 8
      {{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue
  66. 7
      {{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue
  67. 25
      {{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue
  68. 44
      {{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue
  69. 55
      {{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue
  70. 7
      {{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue
  71. 11
      {{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue
  72. 6
      {{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue

2
{{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

6
{{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"

326
{{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"
],
"version": "==1.11.5"
"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.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"
],
"version": "==17.1.2"
"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": "==18.0.0"
},
"qtconsole": {
"hashes": [
@ -844,9 +954,9 @@
},
"tornado": {
"hashes": [
"sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82"
"sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671"
],
"version": "==6.0a1"
"version": "==6.0b1"
},
"traitlets": {
"hashes": [
@ -879,6 +989,14 @@
],
"version": "==1.3.1"
},
"vulture": {
"hashes": [
"sha256:4b5a8980c338e9c068d43e7164555a1e4c9c7d84961ce2bc6f3ed975f6e5bc9d",
"sha256:524b6b9642d0bbe74ea21478bf260937d1ba9b3b86676ca0b17cd10b4b51ba01"
],
"index": "pypi",
"version": "==1.0"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",

74
{{cookiecutter.project_slug}}/backend/app/alembic.ini

@ -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

42
{{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 ###

2
{{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)

25
{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py

@ -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}

64
{{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"}

145
{{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)
):
"""
Retrieve users
"""
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 = 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,
def read_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: UserInDB = Depends(get_current_user),
current_user: DBUser = 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.
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 = 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

17
{{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"}

0
{{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py

5
{{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

30
{{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

2
{{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__)

2
{{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__)

4
{{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")

29
{{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

4
{{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)

98
{{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
def get_user(username, db_session):
return db_session.query(User).filter(User.id == username).first()
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 check_if_user_is_active(user):
return user.is_active
def check_if_user_is_superuser(user):
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_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 get(db_session, *, user_id: int) -> Union[User, None]:
return db_session.query(User).filter(User.id == user_id).first()
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_by_email(db_session, *, email: str) -> Union[User, None]:
return db_session.query(User).filter(User.email == email).first()
def get_user_by_username(username, db_session) -> User:
user = db_session.query(User).filter(User.email == username).first() # type: User
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 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 is_active(user) -> bool:
return user.is_active
def get_user_id(user):
return user.id
def is_superuser(user) -> bool:
return user.is_superuser
def get_users(db_session):
return db_session.query(User).all()
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 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)

214
{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py

@ -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)

3
{{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

106
{{cookiecutter.project_slug}}/backend/app/app/db/full_text_search_utils.py

@ -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"

24
{{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)

4
{{cookiecutter.project_slug}}/backend/app/app/db/external_session.py → {{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)

11
{{cookiecutter.project_slug}}/backend/app/app/db_models/base_relations.py

@ -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")),
)

19
{{cookiecutter.project_slug}}/backend/app/app/db_models/role.py

@ -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")

23
{{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]

21
{{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()

20
{{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

1
{{cookiecutter.project_slug}}/backend/app/app/models/config.py

@ -1 +0,0 @@
USERPROFILE_DOC_TYPE = "userprofile"

14
{{cookiecutter.project_slug}}/backend/app/app/models/role.py

@ -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]

2
{{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

24
{{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

15
{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users.json

@ -1,15 +0,0 @@
{
"name": "users",
"type": "fulltext-alias",
"params": {
"targets": {
"users_01": {}
}
},
"sourceType": "nil",
"sourceName": "",
"sourceUUID": "",
"sourceParams": null,
"planParams": {},
"uuid": ""
}

157
{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users_01.json

@ -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": ""
}

2
{{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

55
{{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

7
{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_get_ids.py

@ -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"

86
{{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)

4
{{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__)

87
{{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

11
{{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)

27
{{cookiecutter.project_slug}}/backend/app/backend-start.sh

@ -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

6
{{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

1
{{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

6
{{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"]

2
{{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:

2
{{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:

4
{{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

3
{{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:

28
{{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",

11
{{cookiecutter.project_slug}}/frontend/src/api.ts

@ -27,17 +27,14 @@ export const api = {
async getUsers(token: string) {
return axios.get<IUserProfile[]>(`${apiUrl}/api/v1/users/`, authHeaders(token));
},
async updateUser(token: string, name: string, data: IUserProfileUpdate) {
return axios.put(`${apiUrl}/api/v1/users/${name}`, data, authHeaders(token));
async updateUser(token: string, userId: number, data: IUserProfileUpdate) {
return axios.put(`${apiUrl}/api/v1/users/${userId}`, data, authHeaders(token));
},
async createUser(token: string, data: IUserProfileCreate) {
return axios.post(`${apiUrl}/api/v1/users/`, data, authHeaders(token));
},
async getRoles(token: string) {
return axios.get(`${apiUrl}/api/v1/roles/`, authHeaders(token));
},
async passwordRecovery(username: string) {
return axios.post(`${apiUrl}/api/v1/password-recovery/${username}`);
async passwordRecovery(email: string) {
return axios.post(`${apiUrl}/api/v1/password-recovery/${email}`);
},
async resetPassword(password: string, token: string) {
return axios.post(`${apiUrl}/api/v1/reset-password/`, {

28
{{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;
}

2
{{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'),

1
{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/commit.ts

@ -5,6 +5,5 @@ import { AdminState } from '../state';
const {commit} = getStoreAccessors<AdminState, State>('');
export const commitSetRoles = commit(mutations.setRoles);
export const commitSetUser = commit(mutations.setUser);
export const commitSetUsers = commit(mutations.setUsers);

1
{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/dispatch.ts

@ -6,6 +6,5 @@ import { actions } from '../actions';
const {dispatch} = getStoreAccessors<AdminState, State>('');
export const dispatchCreateUser = dispatch(actions.actionCreateUser);
export const dispatchGetRoles = dispatch(actions.actionGetRoles);
export const dispatchGetUsers = dispatch(actions.actionGetUsers);
export const dispatchUpdateUser = dispatch(actions.actionUpdateUser);

1
{{cookiecutter.project_slug}}/frontend/src/store/admin/accessors/read.ts

@ -6,5 +6,4 @@ import { getters } from '../getters';
const { read } = getStoreAccessors<AdminState, State>('');
export const readAdminOneUser = read(getters.adminOneUser);
export const readAdminRoles = read(getters.adminRoles);
export const readAdminUsers = read(getters.adminUsers);

13
{{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);
}
},
};

5
{{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] };
}

1
{{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 = {

5
{{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;
},
};

1
{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts

@ -2,5 +2,4 @@ import { IUserProfile } from '@/interfaces';
export interface AdminState {
users: IUserProfile[];
roles: string[];
}

1
{{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';

2
{{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,

8
{{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;
}
}
}

7
{{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);
}
}
</script>

25
{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue

@ -11,15 +11,13 @@
<template slot="items" slot-scope="props">
<td>{{ props.item.name }}</td>
<td>{{ props.item.email }}</td>
<td>{{ props.item.human_name }}</td>
<td>{{ props.item.disabled }}</td>
<td>
<v-chip v-for="role in props.item.admin_roles" :key="role">{{role}}</v-chip>
</td>
<td>{{ props.item.full_name }}</td>
<td><v-icon v-if="props.item.is_active">checkmark</v-icon></td>
<td><v-icon v-if="props.item.is_superuser">checkmark</v-icon></td>
<td class="justify-center layout px-0">
<v-tooltip top>
<span>Edit</span>
<v-btn slot="activator" flat :to="{name: 'main-admin-users-edit', params: {name: props.item.name}}">
<v-btn slot="activator" flat :to="{name: 'main-admin-users-edit', params: {id: props.item.id}}">
<v-icon>edit</v-icon>
</v-btn>
</v-tooltip>
@ -36,7 +34,7 @@ import { IUserProfile } from '@/interfaces';
import { readAdminUsers, dispatchGetUsers } from '@/store/admin/accessors';
@Component
export default class UserProfile extends Vue {
export default class AdminUsers extends Vue {
public headers = [
{
text: 'Name',
@ -53,23 +51,24 @@ export default class UserProfile extends Vue {
{
text: 'Full Name',
sortable: true,
value: 'human_name',
value: 'full_name',
align: 'left',
},
{
text: 'Disabled',
text: 'Is Active',
sortable: true,
value: 'disabled',
value: 'isActive',
align: 'left',
},
{
text: 'Roles',
value: 'admin_roles',
text: 'Is Superuser',
sortable: true,
value: 'isSuperuser',
align: 'left',
},
{
text: 'Actions',
value: 'name',
value: 'id',
},
];
get users() {

44
{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue

@ -7,13 +7,12 @@
<v-card-text>
<template>
<v-form v-model="valid" ref="form" lazy-validation>
<v-text-field label="Username" v-model="name" required></v-text-field>
<v-text-field label="Full Name" v-model="fullName" required></v-text-field>
<v-text-field label="E-mail" type="email" v-model="email" v-validate="'required|email'" data-vv-name="email" :error-messages="errors.collect('email')" required></v-text-field>
<div class="subheading secondary--text text--lighten-2">Roles</div>
<v-checkbox v-for="(value, role) in selectedRoles" :key="role" :label="role" v-model="selectedRoles[role]"></v-checkbox>
<div class="subheading secondary--text text--lighten-2">Disable User <span v-if="userDisabled">(currently disabled)</span><span v-else>(currently enabled)</span></div>
<v-checkbox :label="'Disabled'" v-model="userDisabled"></v-checkbox>
<div class="subheading secondary--text text--lighten-2">User is superuser <span v-if="isSuperuser">(currently is a superuser)</span><span v-else>(currently is not a superuser)</span></div>
<v-checkbox label="Is Superuser" v-model="isSuperuser"></v-checkbox>
<div class="subheading secondary--text text--lighten-2">User is active <span v-if="isActive">(currently active)</span><span v-else>(currently not active)</span></div>
<v-checkbox label="Is Active" v-model="isActive"></v-checkbox>
<v-layout align-center>
<v-flex>
<v-text-field type="password" ref="password" label="Set Password" data-vv-name="password" data-vv-delay="100" v-validate="{required: true}" v-model="password1" :error-messages="errors.first('password')">
@ -41,38 +40,32 @@ import {
IUserProfileUpdate,
IUserProfileCreate,
} from '@/interfaces';
import { dispatchGetUsers, dispatchGetRoles, dispatchCreateUser, readAdminRoles } from '@/store/admin/accessors';
import { dispatchGetUsers, dispatchCreateUser } from '@/store/admin/accessors';
@Component
export default class EditUser extends Vue {
export default class CreateUser extends Vue {
public valid = false;
public name: string = '';
public fullName: string = '';
public email: string = '';
public isActive: boolean = true;
public isSuperuser: boolean = false;
public setPassword = false;
public password1: string = '';
public password2: string = '';
public userDisabled: boolean = false;
public selectedRoles: { [role: string]: boolean } = {};
public async mounted() {
await dispatchGetUsers(this.$store);
await dispatchGetRoles(this.$store);
this.reset();
}
public reset() {
this.password1 = '';
this.password2 = '';
this.name = '';
this.fullName = '';
this.email = '';
this.userDisabled = false;
this.isActive = true;
this.isSuperuser = false;
this.$validator.reset();
this.availableRoles.forEach((value) => {
Vue.set(this.selectedRoles, value, false);
});
}
public cancel() {
@ -82,29 +75,20 @@ export default class EditUser extends Vue {
public async submit() {
if (await this.$validator.validateAll()) {
const updatedProfile: IUserProfileCreate = {
name: this.name,
email: this.email,
};
if (this.fullName) {
updatedProfile.human_name = this.fullName;
updatedProfile.full_name = this.fullName;
}
if (this.email) {
updatedProfile.email = this.email;
}
updatedProfile.disabled = this.userDisabled;
updatedProfile.admin_roles = [];
this.availableRoles.forEach((role: string) => {
if (this.selectedRoles[role]) {
updatedProfile.admin_roles!.push(role);
}
});
updatedProfile.is_active = this.isActive;
updatedProfile.is_superuser = this.isSuperuser;
updatedProfile.password = this.password1;
await dispatchCreateUser(this.$store, updatedProfile);
this.$router.push('/main/admin/users');
}
}
get availableRoles() {
return readAdminRoles(this.$store);
}
}
</script>

55
{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue

@ -8,16 +8,16 @@
<template>
<div class="my-3">
<div class="subheading secondary--text text--lighten-2">Username</div>
<div class="title primary--text text--darken-2" v-if="user">{{user.name}}</div>
<div class="title primary--text text--darken-2" v-if="user">{{user.email}}</div>
<div class="title primary--text text--darken-2" v-else>-----</div>
</div>
<v-form v-model="valid" ref="form" lazy-validation>
<v-text-field label="Full Name" v-model="fullName" required></v-text-field>
<v-text-field label="E-mail" type="email" v-model="email" v-validate="'required|email'" data-vv-name="email" :error-messages="errors.collect('email')" required></v-text-field>
<div class="subheading secondary--text text--lighten-2">Roles</div>
<v-checkbox v-for="(value, role) in selectedRoles" :key="role" :label="role" v-model="selectedRoles[role]"></v-checkbox>
<div class="subheading secondary--text text--lighten-2">Disable User <span v-if="userDisabled">(currently disabled)</span><span v-else>(currently enabled)</span></div>
<v-checkbox :label="'Disabled'" v-model="userDisabled"></v-checkbox>
<div class="subheading secondary--text text--lighten-2">User is superuser <span v-if="isSuperuser">(currently is a superuser)</span><span v-else>(currently is not a superuser)</span></div>
<v-checkbox label="Is Superuser" v-model="isSuperuser"></v-checkbox>
<div class="subheading secondary--text text--lighten-2">User is active <span v-if="isActive">(currently active)</span><span v-else>(currently not active)</span></div>
<v-checkbox label="Is Active" v-model="isActive"></v-checkbox>
<v-layout align-center>
<v-flex shrink>
<v-checkbox v-model="setPassword" class="mr-2"></v-checkbox>
@ -48,31 +48,23 @@ import { Component, Vue } from 'vue-property-decorator';
import { IUserProfile, IUserProfileUpdate } from '@/interfaces';
import {
dispatchGetUsers,
dispatchGetRoles,
dispatchUpdateUser,
readAdminOneUser,
readAdminRoles,
} from '@/store/admin/accessors';
@Component
export default class EditUser extends Vue {
public valid = true;
public name: string = '';
public fullName: string = '';
public email: string = '';
public isActive: boolean = true;
public isSuperuser: boolean = false;
public setPassword = false;
public password1: string = '';
public password2: string = '';
public userDisabled: boolean = false;
public selectedRoles: { [role: string]: boolean } = {};
public async mounted() {
await dispatchGetUsers(this.$store);
await dispatchGetRoles(this.$store);
this.availableRoles.forEach((value) => {
Vue.set(this.selectedRoles, value, false);
});
this.reset();
}
@ -82,17 +74,10 @@ export default class EditUser extends Vue {
this.password2 = '';
this.$validator.reset();
if (this.user) {
this.name = this.user.name;
this.fullName = this.user.human_name;
this.fullName = this.user.full_name;
this.email = this.user.email;
this.userDisabled = this.user.disabled;
this.availableRoles.forEach((role: string) => {
if (this.user!.admin_roles.includes(role)) {
Vue.set(this.selectedRoles, role, true);
} else {
Vue.set(this.selectedRoles, role, false);
}
});
this.isActive = this.user.is_active;
this.isSuperuser = this.user.is_superuser;
}
}
@ -104,33 +89,23 @@ export default class EditUser extends Vue {
if (await this.$validator.validateAll()) {
const updatedProfile: IUserProfileUpdate = {};
if (this.fullName) {
updatedProfile.human_name = this.fullName;
updatedProfile.full_name = this.fullName;
}
if (this.email) {
updatedProfile.email = this.email;
}
updatedProfile.disabled = this.userDisabled;
updatedProfile.admin_roles = [];
this.availableRoles.forEach((role: string) => {
if (this.selectedRoles[role]) {
updatedProfile.admin_roles!.push(role);
}
});
updatedProfile.is_active = this.isActive;
updatedProfile.is_superuser = this.isSuperuser;
if (this.setPassword) {
updatedProfile.password = this.password1;
}
const payload = { name: this.name, user: updatedProfile };
await dispatchUpdateUser(this.$store, payload);
await dispatchUpdateUser(this.$store, { id: this.user!.id, user: updatedProfile });
this.$router.push('/main/admin/users');
}
}
get user() {
return readAdminOneUser(this.$store)(this.$router.currentRoute.params.name);
}
get availableRoles() {
return readAdminRoles(this.$store);
return readAdminOneUser(this.$store)(+this.$router.currentRoute.params.id);
}
}
</script>

7
{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue

@ -7,12 +7,7 @@
<v-card-text>
<div class="my-4">
<div class="subheading secondary--text text--lighten-3">Full Name</div>
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.human_name">{{userProfile.human_name}}</div>
<div class="title primary--text text--darken-2" v-else>-----</div>
</div>
<div class="my-3">
<div class="subheading secondary--text text--lighten-3">Username</div>
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.name">{{userProfile.name}}</div>
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.full_name">{{userProfile.full_name}}</div>
<div class="title primary--text text--darken-2" v-else>-----</div>
</div>
<div class="my-3">

11
{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue

@ -6,11 +6,6 @@
</v-card-title>
<v-card-text>
<template>
<div class="my-3">
<div class="subheading secondary--text text--lighten-2">Username</div>
<div class="title primary--text text--darken-2" v-if="userProfile.name">{{userProfile.name}}</div>
<div class="title primary--text text--darken-2" v-else>-----</div>
</div>
<v-form v-model="valid" ref="form" lazy-validation>
<v-text-field label="Full Name" v-model="fullName" required></v-text-field>
<v-text-field label="E-mail" type="email" v-model="email" v-validate="'required|email'" data-vv-name="email" :error-messages="errors.collect('email')" required></v-text-field>
@ -41,7 +36,7 @@ export default class UserProfileEdit extends Vue {
public created() {
const userProfile = readUserProfile(this.$store);
if (userProfile) {
this.fullName = userProfile.human_name;
this.fullName = userProfile.full_name;
this.email = userProfile.email;
}
}
@ -53,7 +48,7 @@ export default class UserProfileEdit extends Vue {
public reset() {
const userProfile = readUserProfile(this.$store);
if (userProfile) {
this.fullName = userProfile.human_name;
this.fullName = userProfile.full_name;
this.email = userProfile.email;
}
}
@ -66,7 +61,7 @@ export default class UserProfileEdit extends Vue {
if ((this.$refs.form as any).validate()) {
const updatedProfile: IUserProfileUpdate = {};
if (this.fullName) {
updatedProfile.human_name = this.fullName;
updatedProfile.full_name = this.fullName;
}
if (this.email) {
updatedProfile.email = this.email;

6
{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue

@ -7,9 +7,9 @@
<v-card-text>
<template>
<div class="my-3">
<div class="subheading secondary--text text--lighten-2">Username</div>
<div class="title primary--text text--darken-2" v-if="userProfile.name">{{userProfile.name}}</div>
<div class="title primary--text text--darken-2" v-else>-----</div>
<div class="subheading secondary--text text--lighten-2">User</div>
<div class="title primary--text text--darken-2" v-if="userProfile.full_name">{{userProfile.full_name}}</div>
<div class="title primary--text text--darken-2" v-else>{{userProfile.email}}</div>
</div>
<v-form ref="form">
<v-text-field

Loading…
Cancel
Save