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 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.: * After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
```bash ```bash

6
{{cookiecutter.project_slug}}/backend/app/Pipfile

@ -11,6 +11,7 @@ isort = "*"
autoflake = "*" autoflake = "*"
flake8 = "*" flake8 = "*"
pytest = "*" pytest = "*"
vulture = "*"
[packages] [packages]
fastapi = "*" fastapi = "*"
@ -25,6 +26,11 @@ tenacity = "*"
pydantic = "*" pydantic = "*"
emails = "*" emails = "*"
raven = "*" raven = "*"
gunicorn = "*"
jinja2 = "*"
psycopg2-binary = "*"
alembic = "*"
sqlalchemy = "*"
[requires] [requires]
python_version = "3.6" python_version = "3.6"

326
{{cookiecutter.project_slug}}/backend/app/Pipfile.lock

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "0560932caf400303d4621f7725b1e723464a3e4fe00b5a3c031739d41a5ce5fe" "sha256": "9e6b6eaf001ef1b6097d2ecccae8151ade81f5c4ac0f02791ec2248008ddcddf"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -16,6 +16,13 @@
] ]
}, },
"default": { "default": {
"alembic": {
"hashes": [
"sha256:16505782b229007ae905ef9e0ae6e880fddafa406f086ac7d442c1aaf712f8c2"
],
"index": "pypi",
"version": "==1.0.7"
},
"amqp": { "amqp": {
"hashes": [ "hashes": [
"sha256:16056c952e8029ce8db097edf0d7c2fe2ba9de15d30ba08aee2c5221273d8e23", "sha256:16056c952e8029ce8db097edf0d7c2fe2ba9de15d30ba08aee2c5221273d8e23",
@ -77,40 +84,36 @@
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", "sha256:0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6",
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", "sha256:27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709",
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", "sha256:2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a",
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", "sha256:3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282",
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30", "sha256:358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556",
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", "sha256:37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134",
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", "sha256:39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d",
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b", "sha256:42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da",
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", "sha256:5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1",
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e", "sha256:5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509",
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", "sha256:5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1",
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", "sha256:7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3",
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", "sha256:7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96",
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", "sha256:7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2",
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", "sha256:8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28",
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", "sha256:9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c",
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", "sha256:9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033",
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", "sha256:a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d",
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5", "sha256:aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea",
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", "sha256:bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0",
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", "sha256:bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469",
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", "sha256:bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071",
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", "sha256:c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719",
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", "sha256:d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a",
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2", "sha256:df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6",
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", "sha256:f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a",
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", "sha256:f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf",
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", "sha256:f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729"
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", ],
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", "version": "==1.12.1"
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"version": "==1.11.5"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@ -172,11 +175,19 @@
}, },
"fastapi": { "fastapi": {
"hashes": [ "hashes": [
"sha256:932d7e3d13ef1541b0eeb78576c98a68f15552c44a40ae4fb5816b39184d2307", "sha256:06225ac528daec555d5d8488828c9adc1570c0627800abc52481696b2a5e4d1f",
"sha256:b6485bfbf585c6cb944a9a12ae0c29408f046c32ff0341bd46c6e2f1502d214d" "sha256:b37d74e197e6dbb54e3c397fe6dd270e477daa4b016ebb25366d6c9839aca298"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.2.0" "version": "==0.6.0"
},
"gunicorn": {
"hashes": [
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
],
"index": "pypi",
"version": "==19.9.0"
}, },
"h11": { "h11": {
"hashes": [ "hashes": [
@ -198,6 +209,14 @@
], ],
"version": "==2.8" "version": "==2.8"
}, },
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"index": "pypi",
"version": "==2.10"
},
"kombu": { "kombu": {
"hashes": [ "hashes": [
"sha256:529df9e0ecc0bad9fc2b376c3ce4796c41b482cf697b78b71aea6ebe7ca353c8", "sha256:529df9e0ecc0bad9fc2b376c3ce4796c41b482cf697b78b71aea6ebe7ca353c8",
@ -236,6 +255,45 @@
], ],
"version": "==4.3.1" "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": { "passlib": {
"extras": [ "extras": [
"bcrypt" "bcrypt"
@ -254,6 +312,42 @@
], ],
"version": "==3.3.0" "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": { "pycparser": {
"hashes": [ "hashes": [
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
@ -283,6 +377,14 @@
], ],
"version": "==2.8.0" "version": "==2.8.0"
}, },
"python-editor": {
"hashes": [
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
],
"version": "==1.0.4"
},
"python-multipart": { "python-multipart": {
"hashes": [ "hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43" "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
@ -320,11 +422,18 @@
], ],
"version": "==1.12.0" "version": "==1.12.0"
}, },
"sqlalchemy": {
"hashes": [
"sha256:7dede29f121071da9873e7b8c98091874617858e790dc364ffaab4b09d81216c"
],
"index": "pypi",
"version": "==1.3.0b3"
},
"starlette": { "starlette": {
"hashes": [ "hashes": [
"sha256:7cc05c33d00db3b2ddfd7516a737544ed0a34c9dd0ced94076f29b581ce4f532" "sha256:9d48b35d1fc7521d59ae53c421297ab3878d3c7cd4b75266d77f6c73cccb78bb"
], ],
"version": "==0.10.1" "version": "==0.11.1"
}, },
"tenacity": { "tenacity": {
"hashes": [ "hashes": [
@ -343,25 +452,25 @@
}, },
"uvicorn": { "uvicorn": {
"hashes": [ "hashes": [
"sha256:e84fc3b1e142cec395fb7c1d1a9f3cdc0d455037b96e1bed54b378db1121aaba" "sha256:f27889a332ee5c55b4841b11b2392d00dac079f39063fabc1e13e18ada3eb7ba"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.4.3" "version": "==0.4.5"
}, },
"uvloop": { "uvloop": {
"hashes": [ "hashes": [
"sha256:0ff2e67b693f7d2007466952dbe312075098e8f15364fda27d16e8a7f266d74d", "sha256:198fe0c196056930ec6c4a0a878e531a66d15467ca7c74a875aa90271f0c6e3f",
"sha256:2d0029314dc87312ff8d46c3724363d847e5235403eced5d3f98da80a87f4828", "sha256:1c175f47d34b84e33c0e312f4987c927ea004afc3a5f05d2f0f610d71d0e4c89",
"sha256:32dcc003e1973f3db303494f5f63db11091c86a146053773d81ac5484b10c416", "sha256:1c47f197be8f0a3c651dd20be1e1bd43268186246f246d4e86c91e95a89e4865",
"sha256:4301871418f967d0b13409f1bd10ecc7825a7f183282dcc9e19d08532e6cb2e9", "sha256:3fd4943570d20e8cd4d9f0a3190ebd5cf040e5610b685e05c878128a11f7ad14",
"sha256:7639188ff4466d86cfd4418cd784d1198a8cc913279fb8798a4b12a4d42ad341", "sha256:435e232869923fd2248e4ca0ad73e24a5b4debf40bed9dcde133cfe1bef98a7a",
"sha256:a73649cd043f5d3e3ae471667c790a7ee2295b22fac7bedcae8705158f8ba111", "sha256:9cfdb966ae804c46b96c92207dfd2174935ffc70e706e42e1c94c60d16dbe860",
"sha256:afdf34bf507090e4c7f5108a17240982760356b8aae4edd37180ec4f94c36cbb", "sha256:a585781443eeb2edb858f8c08c503aac237a5f1bebf0c84ea8340cc337afa408",
"sha256:bd7a6db5dbfae0c93e27cb200bb2b9513e21a90a2d4a259b39a9b5446c4d5aa3", "sha256:b296493e033846e46488a6aa227a75c790091f5ee5456ec637bb0badad1e8851",
"sha256:cc27e903da274f76826848832f62e1ec410a43602e1e0cd4f8db8c619b1ee93e", "sha256:c684047c6cf6d697ba37872fb1b4489012ea91f3f802c8fbb9c367c4902e88dc",
"sha256:ec521d14ddcdd9f8d0075d7d1f82e9d8806f7f0a047d2e5bc737e9eddf7f930d" "sha256:da5a59d8812188b57b5783c7fb78891d14dd1050b6259680e0dbd4253d7d0f64"
], ],
"version": "==0.12.0" "version": "==0.12.1"
}, },
"vine": { "vine": {
"hashes": [ "hashes": [
@ -478,11 +587,11 @@
}, },
"flake8": { "flake8": {
"hashes": [ "hashes": [
"sha256:c3ba1e130c813191db95c431a18cb4d20a468e98af7a77e2181b68574481ad36", "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048",
"sha256:fd9ddf503110bf3d8b1d270e8c673aab29ccb3dd6abf29bae1f54e5116ab4a91" "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.7.5" "version": "==3.7.6"
}, },
"ipykernel": { "ipykernel": {
"hashes": [ "hashes": [
@ -493,11 +602,11 @@
}, },
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12", "sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39",
"sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742" "sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"
], ],
"markers": "python_version >= '3.3'", "markers": "python_version >= '3.3'",
"version": "==7.2.0" "version": "==7.3.0"
}, },
"ipython-genutils": { "ipython-genutils": {
"hashes": [ "hashes": [
@ -534,6 +643,7 @@
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
], ],
"index": "pypi",
"version": "==2.10" "version": "==2.10"
}, },
"jsonschema": { "jsonschema": {
@ -622,11 +732,11 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
], ],
"version": "==5.0.0" "markers": "python_version > '2.7'",
"version": "==6.0.0"
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
@ -672,10 +782,10 @@
}, },
"parso": { "parso": {
"hashes": [ "hashes": [
"sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6", "sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9",
"sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94" "sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"
], ],
"version": "==0.3.3" "version": "==0.3.4"
}, },
"pexpect": { "pexpect": {
"hashes": [ "hashes": [
@ -701,17 +811,17 @@
}, },
"prometheus-client": { "prometheus-client": {
"hashes": [ "hashes": [
"sha256:e8c11ff5ca53de6c3d91e1510500611cafd1d247a937ec6c588a0a7cc3bef93c" "sha256:1b38b958750f66f208bcd9ab92a633c0c994d8859c831f7abc1f46724fcee490"
], ],
"version": "==0.5.0" "version": "==0.6.0"
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:88002cc618cacfda8760c4539e76c3b3f148ecdb7035a3d422c7ecdc90c2a3ba", "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
"sha256:c6655a12e9b08edb8cf5aeab4815fd1e1bdea4ad73d3bbf269cf2e0c4eb75d5e", "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
"sha256:df5835fb8f417aa55e5cafadbaeb0cf630a1e824aad16989f9f0493e679ec010" "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
], ],
"version": "==2.0.8" "version": "==2.0.9"
}, },
"ptyprocess": { "ptyprocess": {
"hashes": [ "hashes": [
@ -757,11 +867,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07", "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d" "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.2.0" "version": "==4.3.0"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
@ -772,33 +882,33 @@
}, },
"pyzmq": { "pyzmq": {
"hashes": [ "hashes": [
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8", "sha256:07a03450418694fb07e76a0191b6bc9f411afc8e364ca2062edcf28bb0e51c63",
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349", "sha256:15f0bf7cd80020f165635595e197603aedb37fddf4164ad5ae226afc43242f7b",
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba", "sha256:1756dc72e192c670490e38c788c3a35f901adc74ee436e5131d5a3e85fdd7dc6",
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946", "sha256:1d1eb490da54679d724b08ef3ee04530849023670c4ba7e400ed2cdf906720c4",
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf", "sha256:228402625796821f08706f58cc42a3c51c9897d723550babaefe4feec2b6dacc",
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46", "sha256:264ac9dcee6a7af2bce4b61f2d19e5926118a5caa629b50f107ef6318670a364",
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59", "sha256:2b5a43da65f5dec857184d5c2ce13b80071019e96358f146bdecff7238765bc9",
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786", "sha256:3928534fa00a2aabfcfdb439c08ba37fbe99ab0cf57776c8db8d2b73a51693ba",
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40", "sha256:3d2a295b1086d450981f73d3561ac204a0cc9c8ded386a4a34327d918f3b1d0a",
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7", "sha256:411def5b4cbe6111856040a55c8048df113882e90c57ce88de4a48f0189441ac",
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d", "sha256:4b77e96a7ffc1c5e08eaf274db554f227b31717d086adca1bb42b12ef35a7194",
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3", "sha256:4c87fa3e449e1f4ab9170cdfe8213dc0ba34a11b160e6adecafa892e451a29b6",
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d", "sha256:4fd8621a309db6ec23ef1369f43cdf7a9b0dc217d8ff9ca4095a6e932b379bda",
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce", "sha256:54fe55a1694ffe608c8e4c5183e83cab7a91f3e5c84bd6f188868d6676c12aba",
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca", "sha256:60acabd86808a16a895a247fd2bf7a127284a33562d79687bb5df163cff068b2",
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1", "sha256:618887be4ad754228c0cbba7631f6574608b4430fe93974e6322324f1304fdac",
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed", "sha256:69130efb6efa936de601cb135a8a4eec1caccd4ea2b784237145ff4075c2d3ae",
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf", "sha256:6e7f78eeac82140bde7e60e975c6e6b1b678a4dd377782ab63319c1c78bf3aa1",
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27", "sha256:6ee760cdb84e43574da6b3f2f1fc1251e8acf87253900d28a06451c5f5de39e9",
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8", "sha256:75c87f1dc1e65cea4b709f2ebc78fa18d4b475e41463502aec9cd26208b88e0f",
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810", "sha256:97cb1b7cd2c46e87b0a26651eccd2bbb8c758035efd1635ebb81ac36aa76a88c",
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b", "sha256:abfa774dbadacc849121ed92eae05189d226daab583388b499472e1bbb17ef69",
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19", "sha256:ae3d2627d74195ddc95675f2f814aca998381b73dc4341b9e10e3e191e1bdb0b",
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192", "sha256:b30c339eb58355f51f4f54dd61d785f1ff58c86bca1c3a5916977631d121867b",
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924" "sha256:cbabdced5b137cd56aa22633f13ac5690029a0ad43ab6c05f53206e489178362"
], ],
"version": "==17.1.2" "version": "==18.0.0"
}, },
"qtconsole": { "qtconsole": {
"hashes": [ "hashes": [
@ -844,9 +954,9 @@
}, },
"tornado": { "tornado": {
"hashes": [ "hashes": [
"sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82" "sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671"
], ],
"version": "==6.0a1" "version": "==6.0b1"
}, },
"traitlets": { "traitlets": {
"hashes": [ "hashes": [
@ -879,6 +989,14 @@
], ],
"version": "==1.3.1" "version": "==1.3.1"
}, },
"vulture": {
"hashes": [
"sha256:4b5a8980c338e9c068d43e7164555a1e4c9c7d84961ce2bc6f3ed975f6e5bc9d",
"sha256:524b6b9642d0bbe74ea21478bf260937d1ba9b3b86676ca0b17cd10b4b51ba01"
],
"index": "pypi",
"version": "==1.0"
},
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "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 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.token import router as token_router
from app.api.api_v1.endpoints.user import router as user_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 from app.api.api_v1.endpoints.utils import router as utils_router
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(roles_router)
api_router.include_router(token_router) api_router.include_router(token_router)
api_router.include_router(user_router) api_router.include_router(user_router)
api_router.include_router(utils_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 datetime import timedelta
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm 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 import config
from app.core.jwt import create_access_token, get_current_user from app.core.jwt import create_access_token
from app.crud.user import ( from app.core.security import get_password_hash
authenticate_user, from app.crud import user as crud_user
check_if_user_is_active, from app.db_models.user import User as DBUser
check_if_user_is_superuser,
get_user,
update_user,
)
from app.db.database import get_default_bucket
from app.models.msg import Msg from app.models.msg import Msg
from app.models.token import Token from app.models.token import Token
from app.models.user import User, UserInDB, UserInUpdate from app.models.user import User
from app.utils import ( from app.utils import (
generate_password_reset_token, generate_password_reset_token,
send_reset_password_email, send_reset_password_email,
@ -27,70 +24,73 @@ router = APIRouter()
@router.post("/login/access-token", response_model=Token, tags=["login"]) @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 OAuth2 compatible token login, get an access token for future requests
""" """
bucket = get_default_bucket() user = crud_user.authenticate(
user = authenticate_user(bucket, form_data.username, form_data.password) db, email=form_data.username, password=form_data.password
)
if not user: if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password") 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") raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
return { return {
"access_token": create_access_token( "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", "token_type": "bearer",
} }
@router.post("/login/test-token", tags=["login"], response_model=User) @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 Test access token
""" """
return current_user return current_user
@router.post("/password-recovery/{username}", tags=["login"], response_model=Msg) @router.post("/password-recovery/{email}", tags=["login"], response_model=Msg)
def route_recover_password(username: str): def recover_password(email: str, db: Session = Depends(get_db)):
""" """
Password Recovery Password Recovery
""" """
bucket = get_default_bucket() user = crud_user.get_by_email(db, email=email)
user = get_user(bucket, username)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="The user with this username does not exist in the system.", 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( 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"} return {"msg": "Password recovery email sent"}
@router.post("/reset-password/", tags=["login"], response_model=Msg) @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 Reset password
""" """
username = verify_password_reset_token(token) email = verify_password_reset_token(token)
if not username: if not email:
raise HTTPException(status_code=400, detail="Invalid token") raise HTTPException(status_code=400, detail="Invalid token")
bucket = get_default_bucket() user = crud_user.get_by_email(db, email=email)
user = get_user(bucket, username)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="The user with this username does not exist in the system.", 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") raise HTTPException(status_code=400, detail="Inactive user")
user_in = UserInUpdate(name=username, password=new_password) hashed_password = get_password_hash(new_password)
user = update_user(bucket, user_in) user.hashed_password = hashed_password
db.add(user)
db.commit()
return {"msg": "Password updated successfully"} 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 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 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 import config
from app.core.jwt import get_current_user from app.crud import user as crud_user
from app.crud.user import ( from app.db_models.user import User as DBUser
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.models.user import User, UserInCreate, UserInDB, UserInUpdate from app.models.user import User, UserInCreate, UserInDB, UserInUpdate
from app.utils import send_new_account_email from app.utils import send_new_account_email
@ -23,116 +17,99 @@ router = APIRouter()
@router.get("/users/", tags=["users"], response_model=List[User]) @router.get("/users/", tags=["users"], response_model=List[User])
def route_users_get( def read_users(
skip: int = 0, limit: int = 100, current_user: UserInDB = Depends(get_current_user) db: Session = Depends(get_db),
):
"""
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,
skip: int = 0, skip: int = 0,
limit: int = 100, 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/ Retrieve users
For typeahead sufix with `*`. For example, a query with: `email:johnd*` will match users with
email `johndoe@example.com`, `johndid@example.net`, etc.
""" """
if not check_if_user_is_active(current_user): if not crud_user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive 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( raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges" status_code=400, detail="The user doesn't have enough privileges"
) )
bucket = get_default_bucket() users = crud_user.get_multi(db, skip=skip, limit=limit)
users = search_users(bucket=bucket, query_string=q, skip=skip, limit=limit)
return users return users
@router.post("/users/", tags=["users"], response_model=User) @router.post("/users/", tags=["users"], response_model=User)
def route_users_post( def create_user(
*, user_in: UserInCreate, current_user: UserInDB = Depends(get_current_user) *,
db: Session = Depends(get_db),
user_in: UserInCreate,
current_user: DBUser = Depends(get_current_user),
): ):
""" """
Create new 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") 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( raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges" status_code=400, detail="The user doesn't have enough privileges"
) )
bucket = get_default_bucket() user = crud_user.get_by_email(db, email=user_in.email)
user = get_user(bucket, user_in.username)
if user: if user:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="The user with this username already exists in the system.", 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: if config.EMAILS_ENABLED and user_in.email:
send_new_account_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 return user
@router.put("/users/me", tags=["users"], response_model=User) @router.put("/users/me", tags=["users"], response_model=User)
def route_users_me_put( def update_user_me(
*, *,
password: str = None, db: Session = Depends(get_db),
full_name: str = None, password: str = Body(None),
email: EmailStr = None, full_name: str = Body(None),
current_user: UserInDB = Depends(get_current_user), email: EmailStr = Body(None),
current_user: DBUser = Depends(get_current_user),
): ):
""" """
Update own 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") 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: if password is not None:
user_in.password = password user_in.password = password
if full_name is not None: if full_name is not None:
user_in.full_name = full_name user_in.full_name = full_name
if email is not None: if email is not None:
user_in.email = email user_in.email = email
bucket = get_default_bucket() user = crud_user.update(db, user=current_user, user_in=user_in)
user = update_user(bucket, user_in)
return user return user
@router.get("/users/me", tags=["users"], response_model=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 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") raise HTTPException(status_code=400, detail="Inactive user")
return current_user return current_user
@router.post("/users/open", tags=["users"], response_model=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(...), password: str = Body(...),
email: EmailStr = Body(None), email: EmailStr = Body(...),
full_name: str = Body(None), full_name: str = Body(None),
): ):
""" """
@ -143,63 +120,61 @@ def route_users_post_open(
status_code=403, status_code=403,
detail="Open user resgistration is forbidden on this server", detail="Open user resgistration is forbidden on this server",
) )
bucket = get_default_bucket() user = crud_user.get_by_email(db, email=email)
user = get_user(bucket, username)
if user: if user:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="The user with this username already exists in the system", detail="The user with this username already exists in the system",
) )
user_in = UserInCreate( user_in = UserInCreate(password=password, email=email, full_name=full_name)
username=username, password=password, email=email, full_name=full_name user = crud_user.create(db, user_in=user_in)
)
user = upsert_user(bucket, user_in, persist_to=1)
return user return user
@router.get("/users/{username}", tags=["users"], response_model=User) @router.get("/users/{user_id}", tags=["users"], response_model=User)
def route_users_id_get( def read_user_by_id(
username: str, current_user: UserInDB = Depends(get_current_user) user_id: int,
current_user: DBUser = Depends(get_current_user),
db: Session = Depends(get_db),
): ):
""" """
Get a specific user by username (email) 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") raise HTTPException(status_code=400, detail="Inactive user")
bucket = get_default_bucket() user = crud_user.get(db, user_id=user_id)
user = get_user(bucket, username)
if user == current_user: if user == current_user:
return user return user
if not check_if_user_is_superuser(current_user): if not crud_user.is_superuser(current_user):
raise HTTPException( raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges" status_code=400, detail="The user doesn't have enough privileges"
) )
return user return user
@router.put("/users/{username}", tags=["users"], response_model=User) @router.put("/users/{user_id}", tags=["users"], response_model=User)
def route_users_put( def update_user(
*, *,
username: str, db: Session = Depends(get_db),
user_id: int,
user_in: UserInUpdate, user_in: UserInUpdate,
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
): ):
""" """
Update a 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") 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( raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges" status_code=400, detail="The user doesn't have enough privileges"
) )
bucket = get_default_bucket() user = crud_user.get(db, user_id=user_id)
user = get_user(bucket, username)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="The user with this username does not exist in the system", 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 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 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.celery_app import celery_app
from app.core.jwt import get_current_user from app.crud import user as crud_user
from app.crud.user import check_if_user_is_superuser
from app.models.msg import Msg from app.models.msg import Msg
from app.models.user import UserInDB from app.models.user import UserInDB
from app.utils import send_test_email 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) @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 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") raise HTTPException(status_code=400, detail="Not a superuser")
celery_app.send_task("app.worker.test_celery", args=[msg.msg]) celery_app.send_task("app.worker.test_celery", args=[msg.msg])
return {"msg": "Word received"} return {"msg": "Word received"}
@router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201) @router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201)
def route_test_email( def test_email(email_to: EmailStr, current_user: UserInDB = Depends(get_current_user)):
email_to: EmailStr, current_user: UserInDB = Depends(get_current_user)
):
""" """
Test emails 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") raise HTTPException(status_code=400, detail="Not a superuser")
send_test_email(email_to=email_to) send_test_email(email_to=email_to)
return {"msg": "Test email sent"} 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 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) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) 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 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) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) 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") SERVER_HOST = os.getenv("SERVER_HOST")
BACKEND_CORS_ORIGINS = os.getenv( BACKEND_CORS_ORIGINS = os.getenv(
"BACKEND_CORS_ORIGINS" "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") PROJECT_NAME = os.getenv("PROJECT_NAME")
SENTRY_DSN = os.getenv("SENTRY_DSN") SENTRY_DSN = os.getenv("SENTRY_DSN")
@ -47,8 +47,6 @@ EMAIL_RESET_TOKEN_EXPIRE_HOURS = 48
EMAIL_TEMPLATES_DIR = "/app/app/email-templates/build" EMAIL_TEMPLATES_DIR = "/app/app/email-templates/build"
EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL
ROLE_SUPERUSER = "superuser"
FIRST_SUPERUSER = os.getenv("FIRST_SUPERUSER") FIRST_SUPERUSER = os.getenv("FIRST_SUPERUSER")
FIRST_SUPERUSER_PASSWORD = os.getenv("FIRST_SUPERUSER_PASSWORD") 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 from datetime import datetime, timedelta
import jwt 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.core import config
from app.crud.user import get_user
from app.db.database import get_default_bucket
from app.models.token import TokenPayload
ALGORITHM = "HS256" ALGORITHM = "HS256"
access_token_jwt_subject = "access" 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): def create_access_token(*, data: dict, expires_delta: timedelta = None):
to_encode = data.copy() to_encode = data.copy()
@ -40,5 +15,5 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
else: else:
expire = datetime.utcnow() + timedelta(minutes=15) expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire, "sub": access_token_jwt_subject}) 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 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") 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) return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password): def get_password_hash(password: str):
return pwd_context.hash(password) 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 typing import List, Union
from app.models.role import Role
from app.models.user import User
from fastapi.encoders import jsonable_encoder
def get_user(username, db_session): from app.core.security import get_password_hash, verify_password
return db_session.query(User).filter(User.id == username).first() from app.db_models.user import User
from app.models.user import UserInCreate, UserInUpdate
def check_if_user_is_active(user): def get(db_session, *, user_id: int) -> Union[User, None]:
return user.is_active return db_session.query(User).filter(User.id == user_id).first()
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 create_role(name, db_session): def get_by_email(db_session, *, email: str) -> Union[User, None]:
role = Role(name=name) return db_session.query(User).filter(User.email == email).first()
db_session.add(role)
db_session.commit()
return role
def get_roles(db_session):
return db_session.query(Role).all()
def get_user_roles(user):
return user.roles
def get_user_by_username(username, db_session) -> User: def authenticate(db_session, *, email: str, password: str) -> Union[User, bool]:
user = db_session.query(User).filter(User.email == username).first() # type: User 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 return user
def get_user_by_id(user_id, db_session): def is_active(user) -> bool:
user = db_session.query(User).filter(User.id == user_id).first() # type: User return user.is_active
return user
def get_user_hashed_password(user):
return user.password
def get_user_id(user): def is_superuser(user) -> bool:
return user.id return user.is_superuser
def get_users(db_session): def get_multi(db_session, *, skip=0, limit=100) -> Union[List[User], List[None]]:
return db_session.query(User).all() return db_session.query(User).offset(skip).limit(limit).all()
def create_user( def create(db_session, *, user_in: UserInCreate) -> User:
db_session, username, password, first_name=None, last_name=None, is_superuser=False
):
user = User( user = User(
email=username, email=user_in.email,
password=get_password_hash(password), hashed_password=get_password_hash(user_in.password),
first_name=first_name, full_name=user_in.full_name,
last_name=last_name, is_superuser=user_in.is_superuser,
is_superuser=is_superuser,
) )
db_session.add(user) db_session.add(user)
db_session.commit() db_session.commit()
@ -83,8 +49,16 @@ def create_user(
return user return user
def assign_role_to_user(role: Role, user: User, db_session): def update(db_session, *, user: User, user_in: UserInUpdate) -> User:
user.roles.append(role) 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.add(user)
db_session.commit() db_session.commit()
db_session.refresh(user) 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 # Import all the models, so that Base has them before being
# imported by Alembic # imported by Alembic
from app.db.base_class import Base # noqa from app.db.base_class import Base # noqa
from app.models.role import Role # noqa from app.db_models.user import User # noqa
from app.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.core import config
from app.db.utils import ( from app.crud import user as crud_user
assign_role_to_user, from app.models.user import UserInCreate
create_role,
create_user,
get_role_by_name,
get_user_by_username,
)
def init_db(db_session): def init_db(db_session):
@ -14,16 +9,11 @@ def init_db(db_session):
# the tables uncommenting the next line # the tables uncommenting the next line
# Base.metadata.create_all(bind=engine) # Base.metadata.create_all(bind=engine)
role = get_role_by_name("default", db_session) user = crud_user.get_by_email(db_session, email=config.FIRST_SUPERUSER)
if not role:
role = create_role("default", db_session)
user = get_user_by_username(config.FIRST_SUPERUSER, db_session)
if not user: if not user:
user = create_user( user_in = UserInCreate(
db_session, email=config.FIRST_SUPERUSER,
config.FIRST_SUPERUSER, password=config.FIRST_SUPERUSER_PASSWORD,
config.FIRST_SUPERUSER_PASSWORD,
is_superuser=True, 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 import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm import scoped_session, sessionmaker
from app.core import config
engine = create_engine(config.SQLALCHEMY_DATABASE_URI, convert_unicode=True) engine = create_engine(config.SQLALCHEMY_DATABASE_URI, convert_unicode=True)
db_session = scoped_session( db_session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine) 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 sqlalchemy import Boolean, Column, Integer, String
from datetime import datetime
# 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.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): class User(Base):
# Own properties
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, default=datetime.utcnow(), index=True) full_name = Column(String, index=True)
first_name = Column(String, index=True)
last_name = Column(String, index=True)
email = Column(String, unique=True, index=True) email = Column(String, unique=True, index=True)
password = Column(String) hashed_password = Column(String)
is_active = Column(Boolean(), default=True) is_active = Column(Boolean(), default=True)
is_superuser = Column(Boolean(), default=False) 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 fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from app.api.api_v1.api import api_router 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 # CORS
origins = [] origins = []
# Set all CORS enabled origins # Set all CORS enabled origins
if BACKEND_CORS_ORIGINS: if config.BACKEND_CORS_ORIGINS:
origins_raw = BACKEND_CORS_ORIGINS.split(",") origins_raw = config.BACKEND_CORS_ORIGINS.split(",")
for origin in origins_raw: for origin in origins_raw:
use_origin = origin.strip() use_origin = origin.strip()
origins.append(use_origin) origins.append(use_origin)
@ -23,4 +25,12 @@ if BACKEND_CORS_ORIGINS:
allow_headers=["*"], 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): 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 pydantic import BaseModel
from app.models.config import USERPROFILE_DOC_TYPE
from app.models.role import RoleEnum
# Shared properties # Shared properties
class UserBase(BaseModel): class UserBase(BaseModel):
email: Optional[str] = None email: Optional[str] = None
admin_roles: Optional[List[Union[str, RoleEnum]]] = None is_active: Optional[bool] = True
admin_channels: Optional[List[Union[str, RoleEnum]]] = None is_superuser: Optional[bool] = False
disabled: Optional[bool] = None full_name: Optional[str] = None
class UserBaseInDB(UserBase): class UserBaseInDB(UserBase):
username: str id: int = None
full_name: Optional[str] = None
# Properties to receive via API on creation # Properties to receive via API on creation
class UserInCreate(UserBaseInDB): class UserInCreate(UserBaseInDB):
email: str
password: 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 # Properties to receive via API on update
@ -39,10 +33,4 @@ class User(UserBaseInDB):
# Additional properties stored in DB # Additional properties stored in DB
class UserInDB(UserBaseInDB): class UserInDB(UserBaseInDB):
type: str = USERPROFILE_DOC_TYPE
hashed_password: str 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() result = r.json()
assert r.status_code == 200 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 import requests
from app.core import config from app.core import config
from app.crud.user import get_user, upsert_user from app.crud import user as crud_user
from app.db.database import get_default_bucket from app.db.session import db_session
from app.models.user import UserInCreate from app.models.user import UserInCreate
from app.tests.utils.user import user_authentication_headers from app.tests.utils.user import user_authentication_headers
from app.tests.utils.utils import get_server_api, random_lower_string 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() current_user = r.json()
assert current_user assert current_user
assert current_user["disabled"] is False assert current_user["is_active"] is True
assert "superuser" in current_user["admin_roles"] assert current_user["is_superuser"]
assert current_user["username"] == config.FIRST_SUPERUSER assert current_user["email"] == config.FIRST_SUPERUSER
def test_create_user_new_email(superuser_token_headers): def test_create_user_new_email(superuser_token_headers):
server_api = get_server_api() server_api = get_server_api()
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
data = {"username": username, "password": password} data = {"email": username, "password": password}
r = requests.post( r = requests.post(
f"{server_api}{config.API_V1_STR}/users/", f"{server_api}{config.API_V1_STR}/users/",
headers=superuser_token_headers, headers=superuser_token_headers,
@ -32,26 +32,25 @@ def test_create_user_new_email(superuser_token_headers):
) )
assert 200 <= r.status_code < 300 assert 200 <= r.status_code < 300
created_user = r.json() created_user = r.json()
bucket = get_default_bucket() user = crud_user.get_by_email(db_session, email=username)
user = get_user(bucket, username) assert user.email == created_user["email"]
assert user.username == created_user["username"]
def test_get_existing_user(superuser_token_headers): def test_get_existing_user(superuser_token_headers):
server_api = get_server_api() server_api = get_server_api()
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=username, email=username, password=password) user_in = UserInCreate(email=username, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1) user_id = user.id
r = requests.get( 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, headers=superuser_token_headers,
) )
assert 200 <= r.status_code < 300 assert 200 <= r.status_code < 300
api_user = r.json() api_user = r.json()
user = get_user(bucket, username) user = crud_user.get_by_email(db_session, email=username)
assert user.username == api_user["username"] assert user.email == api_user["email"]
def test_create_user_existing_username(superuser_token_headers): 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 = random_lower_string()
# username = email # username = email
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=username, email=username, password=password) user_in = UserInCreate(email=username, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1) data = {"email": username, "password": password}
data = {"username": username, "password": password}
r = requests.post( r = requests.post(
f"{server_api}{config.API_V1_STR}/users/", f"{server_api}{config.API_V1_STR}/users/",
headers=superuser_token_headers, headers=superuser_token_headers,
@ -77,11 +75,10 @@ def test_create_user_by_normal_user():
server_api = get_server_api() server_api = get_server_api()
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=username, email=username, password=password) user_in = UserInCreate(email=username, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1)
user_token_headers = user_authentication_headers(server_api, username, password) user_token_headers = user_authentication_headers(server_api, username, password)
data = {"username": username, "password": password} data = {"email": username, "password": password}
r = requests.post( r = requests.post(
f"{server_api}{config.API_V1_STR}/users/", headers=user_token_headers, json=data 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() server_api = get_server_api()
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=username, email=username, password=password) user_in = UserInCreate(email=username, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1)
username2 = random_lower_string() username2 = random_lower_string()
password2 = random_lower_string() password2 = random_lower_string()
user_in2 = UserInCreate(username=username2, email=username2, password=password2) user_in2 = UserInCreate(email=username2, password=password2)
user2 = upsert_user(bucket, user_in, persist_to=1) user2 = crud_user.create(db_session, user_in=user_in2)
r = requests.get( r = requests.get(
f"{server_api}{config.API_V1_STR}/users/", headers=superuser_token_headers 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 assert len(all_users) > 1
for user in all_users: for user in all_users:
assert "username" in user assert "email" in user
assert "admin_roles" 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 fastapi.encoders import jsonable_encoder
from app.crud.user import ( from app.crud import user as crud_user
authenticate_user, from app.db.session import db_session
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.models.user import UserInCreate from app.models.user import UserInCreate
from app.tests.utils.utils import random_lower_string 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(): def test_create_user():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=email, email=email, password=password) user_in = UserInCreate(email=email, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1) assert user.email == email
assert hasattr(user, "username")
assert user.username == email
assert hasattr(user, "hashed_password") assert hasattr(user, "hashed_password")
assert hasattr(user, "type")
assert user.type == "userprofile"
def test_authenticate_user(): def test_authenticate_user():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=email, email=email, password=password) user_in = UserInCreate(email=email, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1) authenticated_user = crud_user.authenticate(
authenticated_user = authenticate_user(bucket, email, password) db_session, email=email, password=password
)
assert authenticated_user assert authenticated_user
assert user.username == authenticated_user.username assert user.email == authenticated_user.email
def test_not_authenticate_user(): def test_not_authenticate_user():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
bucket = get_default_bucket() user = crud_user.authenticate(db_session, email=email, password=password)
user = authenticate_user(bucket, email, password)
assert user is False assert user is False
def test_check_if_user_is_active(): def test_check_if_user_is_active():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=email, email=email, password=password) user_in = UserInCreate(email=email, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1) is_active = crud_user.is_active(user)
is_active = check_if_user_is_active(user)
assert is_active is True assert is_active is True
def test_check_if_user_is_active_inactive(): def test_check_if_user_is_active_inactive():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate( user_in = UserInCreate(email=email, password=password, disabled=True)
username=email, email=email, password=password, disabled=True print(user_in)
) user = crud_user.create(db_session, user_in=user_in)
bucket = get_default_bucket() print(user)
user = upsert_user(bucket, user_in, persist_to=1) is_active = crud_user.is_active(user)
is_active = check_if_user_is_active(user) print(is_active)
assert is_active is False assert is_active
def test_check_if_user_is_superuser(): def test_check_if_user_is_superuser():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate( user_in = UserInCreate(email=email, password=password, is_superuser=True)
username=email, email=email, password=password, admin_roles=[RoleEnum.superuser] user = crud_user.create(db_session, user_in=user_in)
) is_superuser = crud_user.is_superuser(user)
bucket = get_default_bucket()
user = upsert_user(bucket, user_in, persist_to=1)
is_superuser = check_if_user_is_superuser(user)
assert is_superuser is True assert is_superuser is True
def test_check_if_user_is_superuser_normal_user(): def test_check_if_user_is_superuser_normal_user():
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(username=username, email=username, password=password) user_in = UserInCreate(email=username, password=password)
bucket = get_default_bucket() user = crud_user.create(db_session, user_in=user_in)
user = upsert_user(bucket, user_in, persist_to=1) is_superuser = crud_user.is_superuser(user)
is_superuser = check_if_user_is_superuser(user)
assert is_superuser is False assert is_superuser is False
def test_get_user(): def test_get_user():
password = random_lower_string() password = random_lower_string()
username = random_lower_string() username = random_lower_string()
user_in = UserInCreate( user_in = UserInCreate(email=username, password=password, is_superuser=True)
username=username, user = crud_user.create(db_session, user_in=user_in)
email=username, user_2 = crud_user.get(db_session, user_id=user.id)
password=password, assert user.email == user_2.email
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
assert jsonable_encoder(user) == jsonable_encoder(user_2) 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 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
from app.tests.api.api_v1.token.test_token import test_get_access_token from app.tests.api.api_v1.test_token import test_get_access_token
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

87
{{cookiecutter.project_slug}}/backend/app/app/utils.py

@ -1,96 +1,86 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Union from typing import Optional
import emails import emails
import jwt import jwt
from emails.template import JinjaTemplate from emails.template import JinjaTemplate
from jwt.exceptions import InvalidTokenError from jwt.exceptions import InvalidTokenError
from app.core.config import ( from app.core import config
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,
)
password_reset_jwt_subject = "preset" password_reset_jwt_subject = "preset"
def send_email(email_to: str, subject_template="", html_template="", environment={}): 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( message = emails.Message(
subject=JinjaTemplate(subject_template), subject=JinjaTemplate(subject_template),
html=JinjaTemplate(html_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} smtp_options = {"host": config.SMTP_HOST, "port": config.SMTP_PORT}
if SMTP_TLS: if config.SMTP_TLS:
smtp_options["tls"] = True smtp_options["tls"] = True
if SMTP_USER: if config.SMTP_USER:
smtp_options["user"] = SMTP_USER smtp_options["user"] = config.SMTP_USER
if SMTP_PASSWORD: if config.SMTP_PASSWORD:
smtp_options["password"] = SMTP_PASSWORD smtp_options["password"] = config.SMTP_PASSWORD
response = message.send(to=email_to, render=environment, smtp=smtp_options) response = message.send(to=email_to, render=environment, smtp=smtp_options)
logging.info(f"send email result: {response}") logging.info(f"send email result: {response}")
def send_test_email(email_to: str): def send_test_email(email_to: str):
subject = f"{PROJECT_NAME} - Test email" project_name = config.PROJECT_NAME
with open(Path(EMAIL_TEMPLATES_DIR) / "test_email.html") as f: subject = f"{project_name} - Test email"
with open(Path(config.EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
template_str = f.read() template_str = f.read()
send_email( send_email(
email_to=email_to, email_to=email_to,
subject_template=subject, subject_template=subject,
html_template=template_str, 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): def send_reset_password_email(email_to: str, email: str, token: str):
subject = f"{PROJECT_NAME} - Password recovery for user {username}" project_name = config.PROJECT_NAME
with open(Path(EMAIL_TEMPLATES_DIR) / "reset_password.html") as f: 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() template_str = f.read()
if hasattr(token, "decode"): if hasattr(token, "decode"):
use_token = token.decode() use_token = token.decode()
else: else:
use_token = token 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( send_email(
email_to=email_to, email_to=email_to,
subject_template=subject, subject_template=subject,
html_template=template_str, html_template=template_str,
environment={ environment={
"project_name": PROJECT_NAME, "project_name": config.PROJECT_NAME,
"username": username, "username": email,
"email": email_to, "email": email_to,
"valid_hours": EMAIL_RESET_TOKEN_EXPIRE_HOURS, "valid_hours": config.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
"link": link, "link": link,
}, },
) )
def send_new_account_email(email_to: str, username: str, password: str): def send_new_account_email(email_to: str, username: str, password: str):
subject = f"{PROJECT_NAME} - New acccount for user {username}" project_name = config.PROJECT_NAME
with open(Path(EMAIL_TEMPLATES_DIR) / "new_account.html") as f: 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() template_str = f.read()
link = f"{SERVER_HOST}" link = config.SERVER_HOST
send_email( send_email(
email_to=email_to, email_to=email_to,
subject_template=subject, subject_template=subject,
html_template=template_str, html_template=template_str,
environment={ environment={
"project_name": PROJECT_NAME, "project_name": config.PROJECT_NAME,
"username": username, "username": username,
"password": password, "password": password,
"email": email_to, "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): def generate_password_reset_token(email):
delta = timedelta(hours=EMAIL_RESET_TOKEN_EXPIRE_HOURS) delta = timedelta(hours=config.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
now = datetime.utcnow() now = datetime.utcnow()
expires = now + delta expires = now + delta
exp = expires.timestamp() exp = expires.timestamp()
encoded_jwt = jwt.encode( encoded_jwt = jwt.encode(
{ {"exp": exp, "nbf": now, "sub": password_reset_jwt_subject, "email": email},
"exp": exp, config.SECRET_KEY,
"nbf": now,
"sub": password_reset_jwt_subject,
"username": username,
},
SECRET_KEY,
algorithm="HS256", algorithm="HS256",
) )
return encoded_jwt return encoded_jwt
def verify_password_reset_token(token) -> Union[str, bool]: def verify_password_reset_token(token) -> Optional[str]:
try: 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 assert decoded_token["sub"] == password_reset_jwt_subject
return decoded_token["username"] return decoded_token["email"]
except InvalidTokenError: 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 raven import Client
from app.core import config
from app.core.celery_app import celery_app from app.core.celery_app import celery_app
# Import app code client_sentry = Client(config.SENTRY_DSN)
# Absolute imports for Hydrogen (Jupyter Kernel) compatibility
from app.core.config import SENTRY_DSN
client_sentry = Client(SENTRY_DSN)
@celery_app.task(acks_late=True) @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 # Let the DB start
python /app/app/backend_pre_start.py 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 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 isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app
black 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 # For development, Jupyter remote kernel, Hydrogen
# Using inside the container: # Using inside the container:
@ -15,5 +15,3 @@ WORKDIR /app/
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
EXPOSE 80 EXPOSE 80
CMD ["bash", "/app/backend-start.sh"]

2
{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile

@ -1,6 +1,6 @@
FROM python:3.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 # For development, Jupyter remote kernel, Hydrogen
# Using inside the container: # Using inside the container:

2
{{cookiecutter.project_slug}}/backend/tests.dockerfile

@ -1,6 +1,6 @@
FROM python:3.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 # For development, Jupyter remote kernel, Hydrogen
# Using inside the container: # Using inside the container:

4
{{cookiecutter.project_slug}}/docker-compose.shared.depends.yml

@ -2,8 +2,8 @@ version: '3.3'
services: services:
backend: backend:
depends_on: depends_on:
- couchbase - db
celeryworker: celeryworker:
depends_on: depends_on:
- couchbase - db
- queue - queue

3
{{cookiecutter.project_slug}}/docker-compose.test.yml

@ -6,9 +6,8 @@ services:
dockerfile: tests.dockerfile dockerfile: tests.dockerfile
command: bash -c "while true; do sleep 1; done" command: bash -c "while true; do sleep 1; done"
env_file: env_file:
- env-couchbase.env
- env-sync-gateway.env
- env-backend.env - env-backend.env
- env-postgres.env
environment: environment:
- SERVER_NAME=backend - SERVER_NAME=backend
backend: backend:

28
{{cookiecutter.project_slug}}/frontend/package-lock.json

@ -5280,12 +5280,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -5300,17 +5302,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -5427,7 +5432,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -5439,6 +5445,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -5453,6 +5460,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -5460,12 +5468,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -5484,6 +5494,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -5564,7 +5575,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -5576,6 +5588,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -5697,6 +5710,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^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) { async getUsers(token: string) {
return axios.get<IUserProfile[]>(`${apiUrl}/api/v1/users/`, authHeaders(token)); return axios.get<IUserProfile[]>(`${apiUrl}/api/v1/users/`, authHeaders(token));
}, },
async updateUser(token: string, name: string, data: IUserProfileUpdate) { async updateUser(token: string, userId: number, data: IUserProfileUpdate) {
return axios.put(`${apiUrl}/api/v1/users/${name}`, data, authHeaders(token)); return axios.put(`${apiUrl}/api/v1/users/${userId}`, data, authHeaders(token));
}, },
async createUser(token: string, data: IUserProfileCreate) { async createUser(token: string, data: IUserProfileCreate) {
return axios.post(`${apiUrl}/api/v1/users/`, data, authHeaders(token)); return axios.post(`${apiUrl}/api/v1/users/`, data, authHeaders(token));
}, },
async getRoles(token: string) { async passwordRecovery(email: string) {
return axios.get(`${apiUrl}/api/v1/roles/`, authHeaders(token)); return axios.post(`${apiUrl}/api/v1/password-recovery/${email}`);
},
async passwordRecovery(username: string) {
return axios.post(`${apiUrl}/api/v1/password-recovery/${username}`);
}, },
async resetPassword(password: string, token: string) { async resetPassword(password: string, token: string) {
return axios.post(`${apiUrl}/api/v1/reset-password/`, { return axios.post(`${apiUrl}/api/v1/reset-password/`, {

28
{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts

@ -1,27 +1,23 @@
export interface IUserProfile { export interface IUserProfile {
admin_channels: string[];
admin_roles: string[];
disabled: boolean;
email: string; email: string;
human_name: string; is_active: boolean;
name: string; is_superuser: boolean;
full_name: string;
id: number;
} }
export interface IUserProfileUpdate { export interface IUserProfileUpdate {
human_name?: string;
password?: string;
email?: string; email?: string;
admin_channels?: string[]; full_name?: string;
admin_roles?: string[]; password?: string;
disabled?: boolean; is_active?: boolean;
is_superuser?: boolean;
} }
export interface IUserProfileCreate { export interface IUserProfileCreate {
name: string; email: string;
human_name?: string; full_name?: string;
password?: string; password?: string;
email?: string; is_active?: boolean;
admin_channels?: string[]; is_superuser?: boolean;
admin_roles?: string[];
disabled?: 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'), /* webpackChunkName: "main-admin-users" */ './views/main/admin/AdminUsers.vue'),
}, },
{ {
path: 'users/edit/:name', path: 'users/edit/:id',
name: 'main-admin-users-edit', name: 'main-admin-users-edit',
component: () => import( component: () => import(
/* webpackChunkName: "main-admin-users-edit" */ './views/main/admin/EditUser.vue'), /* 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>(''); const {commit} = getStoreAccessors<AdminState, State>('');
export const commitSetRoles = commit(mutations.setRoles);
export const commitSetUser = commit(mutations.setUser); export const commitSetUser = commit(mutations.setUser);
export const commitSetUsers = commit(mutations.setUsers); 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>(''); const {dispatch} = getStoreAccessors<AdminState, State>('');
export const dispatchCreateUser = dispatch(actions.actionCreateUser); export const dispatchCreateUser = dispatch(actions.actionCreateUser);
export const dispatchGetRoles = dispatch(actions.actionGetRoles);
export const dispatchGetUsers = dispatch(actions.actionGetUsers); export const dispatchGetUsers = dispatch(actions.actionGetUsers);
export const dispatchUpdateUser = dispatch(actions.actionUpdateUser); 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>(''); const { read } = getStoreAccessors<AdminState, State>('');
export const readAdminOneUser = read(getters.adminOneUser); export const readAdminOneUser = read(getters.adminOneUser);
export const readAdminRoles = read(getters.adminRoles);
export const readAdminUsers = read(getters.adminUsers); 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 { import {
commitSetUsers, commitSetUsers,
commitSetUser, commitSetUser,
commitSetRoles,
} from './accessors/commit'; } from './accessors/commit';
import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces'; import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces';
import { State } from '../state'; import { State } from '../state';
@ -23,12 +22,12 @@ export const actions = {
await dispatchCheckApiError(context, error); await dispatchCheckApiError(context, error);
} }
}, },
async actionUpdateUser(context: MainContext, payload: { name: string, user: IUserProfileUpdate }) { async actionUpdateUser(context: MainContext, payload: { id: number, user: IUserProfileUpdate }) {
try { try {
const loadingNotification = { content: 'saving', showProgress: true }; const loadingNotification = { content: 'saving', showProgress: true };
commitAddNotification(context, loadingNotification); commitAddNotification(context, loadingNotification);
const response = (await Promise.all([ 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)), await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
]))[0]; ]))[0];
commitSetUser(context, response.data); commitSetUser(context, response.data);
@ -53,12 +52,4 @@ export const actions = {
await dispatchCheckApiError(context, error); 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 = { export const getters = {
adminUsers: (state: AdminState) => state.users, adminUsers: (state: AdminState) => state.users,
adminRoles: (state: AdminState) => state.roles, adminOneUser: (state: AdminState) => (userId: number) => {
adminOneUser: (state: AdminState) => (name: string) => { const filteredUsers = state.users.filter((user) => user.id === userId);
const filteredUsers = state.users.filter((user) => user.name === name);
if (filteredUsers.length > 0) { if (filteredUsers.length > 0) {
return { ...filteredUsers[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 = { const defaultState: AdminState = {
users: [], users: [],
roles: [],
}; };
export const adminModule = { export const adminModule = {

5
{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts

@ -6,11 +6,8 @@ export const mutations = {
state.users = payload; state.users = payload;
}, },
setUser(state: AdminState, payload: IUserProfile) { 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); users.push(payload);
state.users = users; 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 { export interface AdminState {
users: IUserProfile[]; users: IUserProfile[];
roles: string[];
} }

1
{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts

@ -17,7 +17,6 @@ import {
commitAddNotification, commitAddNotification,
} from './accessors'; } from './accessors';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces';
import { State } from '../state'; import { State } from '../state';
import { MainState, AppNotification } 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) => { hasAdminAccess: (state: MainState) => {
return ( return (
state.userProfile && state.userProfile &&
state.userProfile.admin_roles.includes('superuser')); state.userProfile.is_superuser && state.userProfile.is_active);
}, },
loginError: (state: MainState) => state.logInError, loginError: (state: MainState) => state.logInError,
dashboardShowDrawer: (state: MainState) => state.dashboardShowDrawer, 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 { export default class Dashboard extends Vue {
get greetedUser() { get greetedUser() {
const userProfile = readUserProfile(this.$store); const userProfile = readUserProfile(this.$store);
if (userProfile && userProfile.human_name) { if (userProfile && userProfile.full_name) {
if (userProfile.human_name) { if (userProfile.full_name) {
return userProfile.human_name; return userProfile.full_name;
} else { } 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 { Component, Vue } from 'vue-property-decorator';
import { store } from '@/store'; import { store } from '@/store';
import { readHasAdminAccess } from '@/store/main/accessors'; import { readHasAdminAccess } from '@/store/main/accessors';
import { dispatchGetRoles } from '@/store/admin/accessors';
const routeGuardAdmin = async (to, from, next) => { const routeGuardAdmin = async (to, from, next) => {
if (!readHasAdminAccess(store)) { if (!readHasAdminAccess(store)) {
@ -17,7 +16,7 @@ const routeGuardAdmin = async (to, from, next) => {
}; };
@Component @Component
export default class Start extends Vue { export default class Admin extends Vue {
public beforeRouteEnter(to, from, next) { public beforeRouteEnter(to, from, next) {
routeGuardAdmin(to, from, next); routeGuardAdmin(to, from, next);
} }
@ -25,9 +24,5 @@ export default class Start extends Vue {
public beforeRouteUpdate(to, from, next) { public beforeRouteUpdate(to, from, next) {
routeGuardAdmin(to, from, next); routeGuardAdmin(to, from, next);
} }
public async mounted() {
await dispatchGetRoles(this.$store);
}
} }
</script> </script>

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

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

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

@ -7,13 +7,12 @@
<v-card-text> <v-card-text>
<template> <template>
<v-form v-model="valid" ref="form" lazy-validation> <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="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> <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> <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 v-for="(value, role) in selectedRoles" :key="role" :label="role" v-model="selectedRoles[role]"></v-checkbox> <v-checkbox label="Is Superuser" v-model="isSuperuser"></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> <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="'Disabled'" v-model="userDisabled"></v-checkbox> <v-checkbox label="Is Active" v-model="isActive"></v-checkbox>
<v-layout align-center> <v-layout align-center>
<v-flex> <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')"> <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, IUserProfileUpdate,
IUserProfileCreate, IUserProfileCreate,
} from '@/interfaces'; } from '@/interfaces';
import { dispatchGetUsers, dispatchGetRoles, dispatchCreateUser, readAdminRoles } from '@/store/admin/accessors'; import { dispatchGetUsers, dispatchCreateUser } from '@/store/admin/accessors';
@Component @Component
export default class EditUser extends Vue { export default class CreateUser extends Vue {
public valid = false; public valid = false;
public name: string = '';
public fullName: string = ''; public fullName: string = '';
public email: string = ''; public email: string = '';
public isActive: boolean = true;
public isSuperuser: boolean = false;
public setPassword = false; public setPassword = false;
public password1: string = ''; public password1: string = '';
public password2: string = ''; public password2: string = '';
public userDisabled: boolean = false;
public selectedRoles: { [role: string]: boolean } = {};
public async mounted() { public async mounted() {
await dispatchGetUsers(this.$store); await dispatchGetUsers(this.$store);
await dispatchGetRoles(this.$store);
this.reset(); this.reset();
} }
public reset() { public reset() {
this.password1 = ''; this.password1 = '';
this.password2 = ''; this.password2 = '';
this.name = '';
this.fullName = ''; this.fullName = '';
this.email = ''; this.email = '';
this.userDisabled = false; this.isActive = true;
this.isSuperuser = false;
this.$validator.reset(); this.$validator.reset();
this.availableRoles.forEach((value) => {
Vue.set(this.selectedRoles, value, false);
});
} }
public cancel() { public cancel() {
@ -82,29 +75,20 @@ export default class EditUser extends Vue {
public async submit() { public async submit() {
if (await this.$validator.validateAll()) { if (await this.$validator.validateAll()) {
const updatedProfile: IUserProfileCreate = { const updatedProfile: IUserProfileCreate = {
name: this.name, email: this.email,
}; };
if (this.fullName) { if (this.fullName) {
updatedProfile.human_name = this.fullName; updatedProfile.full_name = this.fullName;
} }
if (this.email) { if (this.email) {
updatedProfile.email = this.email; updatedProfile.email = this.email;
} }
updatedProfile.disabled = this.userDisabled; updatedProfile.is_active = this.isActive;
updatedProfile.admin_roles = []; updatedProfile.is_superuser = this.isSuperuser;
this.availableRoles.forEach((role: string) => {
if (this.selectedRoles[role]) {
updatedProfile.admin_roles!.push(role);
}
});
updatedProfile.password = this.password1; updatedProfile.password = this.password1;
await dispatchCreateUser(this.$store, updatedProfile); await dispatchCreateUser(this.$store, updatedProfile);
this.$router.push('/main/admin/users'); this.$router.push('/main/admin/users');
} }
} }
get availableRoles() {
return readAdminRoles(this.$store);
}
} }
</script> </script>

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

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

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

@ -7,12 +7,7 @@
<v-card-text> <v-card-text>
<div class="my-4"> <div class="my-4">
<div class="subheading secondary--text text--lighten-3">Full Name</div> <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-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">
<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-else>-----</div> <div class="title primary--text text--darken-2" v-else>-----</div>
</div> </div>
<div class="my-3"> <div class="my-3">

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

@ -6,11 +6,6 @@
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<template> <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-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="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> <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() { public created() {
const userProfile = readUserProfile(this.$store); const userProfile = readUserProfile(this.$store);
if (userProfile) { if (userProfile) {
this.fullName = userProfile.human_name; this.fullName = userProfile.full_name;
this.email = userProfile.email; this.email = userProfile.email;
} }
} }
@ -53,7 +48,7 @@ export default class UserProfileEdit extends Vue {
public reset() { public reset() {
const userProfile = readUserProfile(this.$store); const userProfile = readUserProfile(this.$store);
if (userProfile) { if (userProfile) {
this.fullName = userProfile.human_name; this.fullName = userProfile.full_name;
this.email = userProfile.email; this.email = userProfile.email;
} }
} }
@ -66,7 +61,7 @@ export default class UserProfileEdit extends Vue {
if ((this.$refs.form as any).validate()) { if ((this.$refs.form as any).validate()) {
const updatedProfile: IUserProfileUpdate = {}; const updatedProfile: IUserProfileUpdate = {};
if (this.fullName) { if (this.fullName) {
updatedProfile.human_name = this.fullName; updatedProfile.full_name = this.fullName;
} }
if (this.email) { if (this.email) {
updatedProfile.email = this.email; updatedProfile.email = this.email;

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

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

Loading…
Cancel
Save