diff --git a/Pipfile b/Pipfile index 8393ed0..69a211c 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,8 @@ verify_ssl = true [dev-packages] autopep8 = "*" pylint = "*" +black = "==19.3b0" +isort = "==4.3.21" [packages] django = "==2.2.3" @@ -18,6 +20,10 @@ psycopg2 = "==2.7.5" django-cors-headers = "==3.0.2" gunicorn = "==19.9.0" django-heroku = "==0.3.1" +django-phonenumber-field = "==3.0.1" +phonenumbers = "==8.10.14" +pyjwt = "==1.7.1" +django-filter = "==2.1.0" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 9ce7275..d564f3b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fa9c6780a0999e892805c25a6e66ad399ab333cf596bc617f40362b5bdb222db" + "sha256": "1869e5eaf276d31490b8bf3f66b33a371c27a0e7f7642c22a7253d5749d919c0" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "babel": { + "hashes": [ + "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", + "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28" + ], + "version": "==2.7.0" + }, "cachecontrol": { "hashes": [ "sha256:cef77effdf51b43178f6a2d3b787e3734f98ade253fa3187f3bb7315aaa42ff7" @@ -75,6 +82,14 @@ "index": "pypi", "version": "==2.1.9" }, + "django-filter": { + "hashes": [ + "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d", + "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68" + ], + "index": "pypi", + "version": "==2.1.0" + }, "django-heroku": { "hashes": [ "sha256:2bc690aab89eedbe01311752320a9a12e7548e3b0ed102681acc5736a41a4762", @@ -83,6 +98,14 @@ "index": "pypi", "version": "==0.3.1" }, + "django-phonenumber-field": { + "hashes": [ + "sha256:1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e", + "sha256:794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97" + ], + "index": "pypi", + "version": "==3.0.1" + }, "djangorestframework": { "hashes": [ "sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651", @@ -103,18 +126,18 @@ "grpc" ], "hashes": [ - "sha256:72a1c8966bdbd70a72de32760368aec399fe6a5c2a6675d9476cb9ae27046de7", - "sha256:f45d74aef41e1de49ceadebf382e1291ddcbe431a8b533aff8a1140a35531465" + "sha256:ae9e76295a268e27a829a752cc2d0e58a529daeecf9be8ff7a981f0325452671", + "sha256:b4e488df53535a1d6f5876c0201f00a954659f5ed396d63fc15b3204d63a3604" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==1.13.0" + "version": "==1.14.0" }, "google-api-python-client": { "hashes": [ - "sha256:048da0d68564380ee23b449e5a67d4666af1b3b536d2fb0a02cee1ad540fa5ec", - "sha256:5def5a485b1cbc998b8f869456c7bde0c0e6d3d0a5ea1f300b5ef57cb4b1ce8f" + "sha256:2e55a5c7b56233c68945b6804c73e253445933f4d485d4e69e321b38772b9dd6", + "sha256:60f2ac2f27997d9af10ae126d9937b7d8c1fd061d12668ccaf94b4347ee85021" ], - "version": "==1.7.9" + "version": "==1.7.10" }, "google-auth": { "hashes": [ @@ -139,18 +162,18 @@ }, "google-cloud-firestore": { "hashes": [ - "sha256:d60aa73389d67d391f6260a1f2b88e234fda66dc35b7a3e95e54bcd0bd1d163e", - "sha256:e95742e5441f5bc1bc3dabd7277e8b62b96b241a6a9566e549df950778db2cc1" + "sha256:8889f9f33f62ff5fe7ee4bfbe4f430ad40fb51eb860f65fccfbebecb5a225f2e", + "sha256:fdbc111b7e4ae99f0cd3826464f8394ecc163cb727362905ef14bef5229bf5ba" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==1.2.0" + "version": "==1.3.0" }, "google-cloud-storage": { "hashes": [ - "sha256:10b8a708d71b45589e06d0dc2bcc614e1d1c921902df8d22e374af1fb346ab68", - "sha256:4a2a0e2486b1977a4f3e382ed0be118933ecb6a92dc6b2c3d0b56ef8d76ac7a9" + "sha256:4e24989b69e37086bd8c219d5502b13540ee9650ab9ea52cd909ad819ea51542", + "sha256:6ed0d1c29124554691d6ff58f406062540fc9d8ef50acd59061ec3d5f820b398" ], - "version": "==1.16.1" + "version": "==1.17.0" }, "google-resumable-media": { "hashes": [ @@ -167,40 +190,40 @@ }, "grpcio": { "hashes": [ - "sha256:0232add03144dd3cf9b660e2718244cb8e175370dca4d3855cb4e489a7811b53", - "sha256:0f20e6dcb1b8662cdca033bb97c0a8116a5343e3ebc7f71c5fe7f89039978350", - "sha256:10b07a623d33d4966f45c85d410bc6a79c5ac6341f06c3beda6c22be12cbfe07", - "sha256:10c0476d5a52d21f402fc073745dc43b87cc8e080a1f49bbff4e1059019310fb", - "sha256:289dae0b35c59d191c524e976dd0a6f8c995d2062e72621eb866ad0f4472a635", - "sha256:2be726f16142d358a0df1e81d583d6820ee561a7856a79cca2fbe49989308be7", - "sha256:4338d2a81f5b4ca022e085040b3cfce19419a5ce44aa7e6810ac1df05365bed7", - "sha256:4c535b46f20e66bee3097583231977e721acdfcb1671d1490c99b7be8902ce18", - "sha256:557154aef70a0e979700cc9528bc8b606b668084a29a0d57dbc4b06b078a2f1c", - "sha256:5bfdd7e6647498f979dc46583723c852d97b25afe995d55aa1c76a5f9816bc1f", - "sha256:87d8943ae7aa6ca5bbad732867d7f17d2550e4966a0c15b52088e8b579422e47", - "sha256:89d8719d8de4d137678f7caa979e1b0a6fd4026f8096ceef8c2d164bbabefaf2", - "sha256:9c3f4af989ce860710ac1864dc2e867dd87e6cee51a2368df1b253596868e52f", - "sha256:9da52c3c728883aee429bb7c315049f50b2139f680cd86bb1165418e4f93a982", - "sha256:9e9736659987beab42d18525ed10d21f80a1ba8389eac03425fbfd5684e6bbf0", - "sha256:9ebcbb1a054cab362d29d3be571d43d6b9b23302d9fc4b43e5327000da1680a9", - "sha256:a93e08636623e24c939851e2e0c0140b14f524b2980c9cdc4ea52b70a871c7e0", - "sha256:ac322d86d1a079e0a118d544443ee16f320af0062c191b4754c0c6ec2fc79310", - "sha256:b1fb101459868f52df6b61e7bb13375e50badf17a160e39fe1d51ae19e53f461", - "sha256:b39aac96cceac624a23d540473835086a3ffa77c91030189988c073488434493", - "sha256:b65507bc273c6dbf539175a786a344cc0ac78d50e5584f72c6599733f8a3301f", - "sha256:be5bb6e47417e537c884a2e2ff2e1a8b2c064a998fcfdfcc67528d4e63e7ebaf", - "sha256:c92de6a28a909c4f460dc1bbbcb50d676cf0b1f40224b222761f73fdd851b522", - "sha256:c9f5962eb7fa7607b20eb0e4f59ed35829bd600fc0eacb626a6db83229a3e445", - "sha256:d00bdf9c546ed6e649f785c55b05288e8b2dbb6bf2eb74b6c579fa0d591d35bd", - "sha256:da804b1dd8293bd9d61b1e6ea989c887ba042a808a4fbdd80001cfa059aafed2", - "sha256:ead6c5aa3e807345913649c3be395aaca2bbb2d225f18b8f31f37eab225508f6", - "sha256:eb4d81550ce6f826af4ec6e8d98be347fe96291d718bf115c3f254621ae8d98d", - "sha256:ef6a18ec8fd32ec81748fe720544ea2fb2d2dc50fd6d06739d5e2eb8f0626a1c", - "sha256:fad42835656e0b6d3b7ffc900598e776722e30f43b7234a48f2576ca30f31a47", - "sha256:fb98dbfee0d963b49ae5754554028cf62e6bd695f22de16d242ba9d2f0b7339b", - "sha256:fb9cd9bb8d26dc17c2dd715a46bca3a879ec8283879b164e85863110dc6e3b2a" - ], - "version": "==1.21.1" + "sha256:03b78b4e7dcdfe3e257bb528cc93923f9cbbab6d5babf15a60d21e9a4a70b1a2", + "sha256:1ce0ccfbdfe84387dbcbf44adb4ae16ec7ae70e166ffab478993eb1ea1cba3ce", + "sha256:22e167a9406d73dd19ffe8ed6a485f17e6eac82505be8c108897f15e68badcbb", + "sha256:31d0aeca8d8ee2301c62c5c340e0889d653b1280d68f9fa203982cb6337b050e", + "sha256:44c7f99ca17ebbcc96fc54ed00b454d8313f1eac28c563098d8b901025aff941", + "sha256:5471444f53f9db6a1f1f11f5dbc173228881df8446380b6b98f90afb8fd8348e", + "sha256:561bca3b1bde6d6564306eb05848fd155136e9c3a25d2961129b1e2edba22fce", + "sha256:5bf58e1d2c2f55365c06e8cb5abe067b88ca2e5550fb62009c41df4b54505acf", + "sha256:6b7163d1e85d76b0815df63fcc310daec02b44532bb433f743142d4febcb181f", + "sha256:766d79cddad95f5f6020037fe60ea8b98578afdf0c59d5a60c106c1bdd886303", + "sha256:770b7372d5ca68308ff66d7baee53369fa5ce985f84bcb6aa1948c1f2f7b02f2", + "sha256:7ab178da777fc0f55b6aef5a755f99726e8e4b75e3903954df07b27059b54fcf", + "sha256:8078305e77c2f6649d36b24d8778096413e474d9d7892c6f92cfb589c9d71b2e", + "sha256:85600b63a386d860eeaa955e9335e18dd0d7e5477e9214825abf2c2884488369", + "sha256:857d9b939ae128be1c0c792eb885c7ff6a386b9dea899ac4b06f4d90a31f9d87", + "sha256:87a41630c90c179fa5c593400f30a467c498972c702f348d41e19dafeb1d319e", + "sha256:8805d486c6128cc0fcc8ecf16c4095d99a8693a541ef851429ab334e028a4a97", + "sha256:8d71b7a89c306a41ccc7741fc9409b14f5b86727455c2a1c0c7cfcb0f784e1f2", + "sha256:9e1b80bd65f8f160880cb4dad7f55697f6d37b2d7f251fc0c2128e811928f369", + "sha256:9e290c84a145ae2411ee0ec9913c41cd7500e2e7485fe93632434d84ef4fda67", + "sha256:9ec9f88b5bc94bd99372f27cdd53af1c92ba06717380b127733b953cfb181174", + "sha256:a0a02a8b4ba6deadf706d5f849539b3685b72b186a3c9ef5d43e8972ed60fb6f", + "sha256:a4059c59519f5940e01a071f74ae2a60ea8f6185b03d22a09d40c7959a36b16b", + "sha256:a6e028c2a6da2ebfa2365a5b32531d311fbfec0e3600fc27e901b64f0ff7e54e", + "sha256:adcdebf9f8463df4120c427cf6c9aed39258bccd03ed37b6939e7a145d64d6e0", + "sha256:bdec982610259d07156a58f80b8c3e69be7751a9208bc577b059c5193d087fad", + "sha256:cefc4d4251ffb73feb303d4b7e9d6c367cb60f2db16d259ea28b114045f965aa", + "sha256:d4145c8aa6afbac10ad27e408f7ce15992fe89ba5d0b4abca31c0c2729864c03", + "sha256:da76dc5ad719ee99de5ea28a5629ff92172cbb4a70d8a6ae3a5b7a53c7382ce1", + "sha256:dde2452c08ef8b6426ccab6b5b6de9f06d836d9937d6870e68153cbf8cb49348", + "sha256:e3d88091d2539a4868750914a6fe7b9ec50e42b913851fc1b77423b5bd918530", + "sha256:f9c67cfe6278499d7f83559dc6322a8bbb108e307817a3d7acbfea807b3603cc" + ], + "version": "==1.22.0" }, "gunicorn": { "hashes": [ @@ -246,28 +269,36 @@ ], "version": "==0.6.1" }, + "phonenumbers": { + "hashes": [ + "sha256:81757480b3553e9aa8b7442f28fc53e7c8335ef8c99c3b292bcd55b08820aa00", + "sha256:cb1d559b3f63cef4e8521f9ebeb4f41a0320f97463f3b11c15bd7d1d3e0f40db" + ], + "index": "pypi", + "version": "==8.10.14" + }, "protobuf": { "hashes": [ - "sha256:03f43eac9d5b651f976e91cf46a25b75e5779d98f0f4114b0abfed83376d75f8", - "sha256:0c94b21e6de01362f91a86b372555d22a60b59708599ca9d5032ae9fdf8e3538", - "sha256:2d2a9f30f61f4063fadd7fb68a2510a6939b43c0d6ceeec5c4704f22225da28e", - "sha256:34a0b05fca061e4abb77dd180209f68d8637115ff319f51e28a6a9382d69853a", - "sha256:358710fd0db25372edcf1150fa691f48376a134a6c69ce29f38f185eea7699e6", - "sha256:41e47198b94c27ba05a08b4a95160656105745c462af574e4bcb0807164065c0", - "sha256:8c61cc8a76e9d381c665aecc5105fa0f1878cf7db8b5cd17202603bcb386d0fc", - "sha256:a6eebc4db759e58fdac02efcd3028b811effac881d8a5bad1996e4e8ee6acb47", - "sha256:a9c12f7c98093da0a46ba76ec40ace725daa1ac4038c41e4b1466afb5c45bb01", - "sha256:cb95068492ba0859b8c9e61fa8ba206a83c64e5d0916fb4543700b2e2b214115", - "sha256:cd98476ce7bb4dcd6a7b101f5eecdc073dafea19f311e36eb8fba1a349346277", - "sha256:ce64cfbea18c535176bdaa10ba740c0fc4c6d998a3f511c17bedb0ae4b3b167c", - "sha256:dcbb59eac73fd454e8f2c5fba9e3d3320fd4707ed6a9d3ea3717924a6f0903ea", - "sha256:dd67f34458ae716029e2a71ede998e9092493b62a519236ca52e3c5202096c87", - "sha256:e3c96056eb5b7284a20e256cb0bf783c8f36ad82a4ae5434a7b7cd02384144a7", - "sha256:f612d584d7a27e2f39e7b17878430a959c1bc09a74ba09db096b468558e5e126", - "sha256:f6de8a7d6122297b81566e5bd4df37fd5d62bec14f8f90ebff8ede1c9726cd0a", - "sha256:fa529d9261682b24c2aaa683667253175c9acebe0a31105394b221090da75832" - ], - "version": "==3.8.0" + "sha256:05c36022fef3c7d3562ac22402965c0c2b9fe8421f459bb377323598996e407f", + "sha256:139b7eadcca0a861d60b523cb37d9475505e0dfb07972436b15407c2b968d87e", + "sha256:15f683006cb77fb849b1f561e509b03dd2b7dcc749086b8dd1831090d0ba4740", + "sha256:2ad566b7b7cdd8717c7af1825e19f09e8fef2787b77fcb979588944657679604", + "sha256:35cfcf97642ef62108e10a9431c77733ec7eaab8e32fe4653de20403429907cb", + "sha256:387822859ecdd012fdc25ec879f7f487da6e1d5b1ae6115e227e6be208836f71", + "sha256:4df14cbe1e7134afcfdbb9f058949e31c466de27d9b2f7fb4da9e0b67231b538", + "sha256:586c4ca37a7146d4822c700059f150ac3445ce0aef6f3ea258640838bb892dc2", + "sha256:58b11e530e954d29ab3180c48dc558a409f705bf16739fd4e0d3e07924ad7add", + "sha256:63c8c98ccb8c95f41c18fb829aeeab21c6249adee4ed75354125bdc44488f30e", + "sha256:72edcbacd0c73eef507d2ff1af99a6c27df18e66a3ff4351e401182e4de62b03", + "sha256:83dc8a561b3b954fd7002c690bb83278b8d1742a1e28abba9aaef28b0c8b437d", + "sha256:913171ecc84c2726b86574e40549a0ea619d569657c5a5ff782a3be7d81401a5", + "sha256:aabb7c741d3416671c3e6fe7c52970a226e6a8274417a97d7d795f953fadef36", + "sha256:b3452bbda12b1cbe2187d416779de07b2ab4c497d83a050e43c344778763721d", + "sha256:c5d5b8d4a9212338297fa1fa44589f69b470c0ba1d38168b432d577176b386a8", + "sha256:d86ee389c2c4fc3cebabb8ce83a8e97b6b3b5dc727b7419c1ccdc7b6e545a233", + "sha256:f2db8c754de788ab8be5e108e1e967c774c0942342b4f8aaaf14063889a6cfdc" + ], + "version": "==3.9.0" }, "psycopg2": { "hashes": [ @@ -319,6 +350,14 @@ ], "version": "==0.2.5" }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "index": "pypi", + "version": "==1.7.1" + }, "python-decouple": { "hashes": [ "sha256:1317df14b43efee4337a4aa02914bf004f010cd56d6c4bd894e6474ec8c4fe2d" @@ -378,13 +417,20 @@ }, "whitenoise": { "hashes": [ - "sha256:118ab3e5f815d380171b100b05b76de2a07612f422368a201a9ffdeefb2251c1", - "sha256:42133ddd5229eeb6a0c9899496bdbe56c292394bf8666da77deeb27454c0456a" + "sha256:59d880d25d0e90bcc6554fe0504a11195bd2e59b3d690b6fb42a8040d4e67ef5", + "sha256:c9b7c47fdc1dba4d37bf2787a01a844dc7a521e174fcd22a2d429e0be65e1782" ], - "version": "==4.1.2" + "version": "==4.1.3" } }, "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, "astroid": { "hashes": [ "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", @@ -392,6 +438,13 @@ ], "version": "==2.2.5" }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, "autopep8": { "hashes": [ "sha256:4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee" @@ -399,11 +452,27 @@ "index": "pypi", "version": "==1.4.4" }, + "black": { + "hashes": [ + "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", + "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + ], + "index": "pypi", + "version": "==19.3b0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], + "index": "pypi", "version": "==4.3.21" }, "lazy-object-proxy": { @@ -458,6 +527,13 @@ ], "version": "==1.12.0" }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, "typed-ast": { "hashes": [ "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", diff --git a/Procfile b/Procfile index b7398ac..1f7e008 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ -web: gunicorn app.wsgi \ No newline at end of file +release: python manage.py migrate +web: gunicorn dscountr.wsgi \ No newline at end of file diff --git a/app/apps.py b/app/apps.py new file mode 100644 index 0000000..1e4d098 --- /dev/null +++ b/app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MyappConfig(AppConfig): + name = "app" diff --git a/app/auth_backend.py b/app/auth_backend.py new file mode 100644 index 0000000..a731dae --- /dev/null +++ b/app/auth_backend.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + +User = get_user_model() + + +class PasswordlessAuthBackend(ModelBackend): + """Log in to Django without providing a password. + + """ + + def authenticate(self, username=None): + try: + return User.objects.get(username=username) + except User.DoesNotExist: + return None + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None diff --git a/app/authentication/__init__.py b/app/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/authentication/admin.py b/app/authentication/admin.py new file mode 100644 index 0000000..0dcbec1 --- /dev/null +++ b/app/authentication/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register models here. diff --git a/app/authentication/backends.py b/app/authentication/backends.py new file mode 100644 index 0000000..5ba230a --- /dev/null +++ b/app/authentication/backends.py @@ -0,0 +1,74 @@ +import jwt + +from django.conf import settings +from datetime import datetime + +from decouple import config + +from rest_framework import authentication, exceptions + +from .models import User + + +class JWTAuthentication(authentication.BaseAuthentication): + authentication_header_prefix = 'Token' + + def authenticate(self, request): + """ + The `authenticate` method is called on every request regardless of + whether the endpoint requires authentication. + """ + request.user = None + + auth_header = authentication.get_authorization_header(request).split() + auth_header_prefix = self.authentication_header_prefix.lower() + + if not auth_header: + return None + + if len(auth_header) == 1: + return None + + elif len(auth_header) > 2: + return None + + prefix = auth_header[0].decode('utf-8') + token = auth_header[1].decode('utf-8') + + if prefix.lower() != auth_header_prefix: + return None + + return self._authenticate_credentials(request, token) + + def _authenticate_credentials(self, request, token): + """ + Try to authenticate the given credentials. If authentication is + successful, return the user and token. If not, throw an error. + """ + try: + payload = jwt.decode(token, config('SECRET_KEY')) + except: + msg = 'Invalid authentication. Could not decode token.' + raise exceptions.AuthenticationFailed(msg) + + try: + user = User.objects.get(pk=payload['id']) + except User.DoesNotExist: + msg = 'No user matching this token was found.' + raise exceptions.AuthenticationFailed(msg) + + if not user.is_active: + msg = 'This user has been deactivated.' + raise exceptions.AuthenticationFailed(msg) + + return (user, token) + + def generate_token(self, user_id): + """ + gerates jwt token by encoding registered user id + """ + payload = { + 'id': user_id, + 'iat': datetime.utcnow() + } + return jwt.encode(payload, config('SECRET_KEY')).decode('UTF-8') diff --git a/app/authentication/choices.py b/app/authentication/choices.py new file mode 100644 index 0000000..933a2b7 --- /dev/null +++ b/app/authentication/choices.py @@ -0,0 +1 @@ +GENDER_CHOICES = (("M", "Male"), ("F", "Female"), ("A", "Alien")) diff --git a/app/authentication/migrations/0001_initial.py b/app/authentication/migrations/0001_initial.py new file mode 100644 index 0000000..b06415b --- /dev/null +++ b/app/authentication/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2.3 on 2019-07-02 20:00 + +import app.authentication.models +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(db_index=True, max_length=255, unique=True)), + ('email', models.EmailField(db_index=True, max_length=254, unique=True)), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(db_index=True, max_length=128, region=None, unique=True)), + ('date_of_birth', models.DateField()), + ('gender', models.CharField(choices=[('M', 'Male'), ('F', 'Female')], max_length=1)), + ('password', models.CharField(max_length=128)), + ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name_plural': 'All Users', + }, + managers=[ + ('objects', app.authentication.models.UserManager()), + ], + ), + ] diff --git a/app/authentication/migrations/0002_auto_20190708_1300.py b/app/authentication/migrations/0002_auto_20190708_1300.py new file mode 100644 index 0000000..44ec40f --- /dev/null +++ b/app/authentication/migrations/0002_auto_20190708_1300.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-07-08 13:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='gender', + field=models.CharField(choices=[('M', 'Male'), ('F', 'Female'), ('A', 'Alien')], max_length=1), + ), + ] diff --git a/app/authentication/migrations/0003_auto_20190711_1054.py b/app/authentication/migrations/0003_auto_20190711_1054.py new file mode 100644 index 0000000..3ff11c6 --- /dev/null +++ b/app/authentication/migrations/0003_auto_20190711_1054.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-07-11 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0002_auto_20190708_1300'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='password', + field=models.CharField(max_length=128, verbose_name='password'), + ), + ] diff --git a/app/authentication/migrations/0004_auto_20190711_1539.py b/app/authentication/migrations/0004_auto_20190711_1539.py new file mode 100644 index 0000000..3d9748a --- /dev/null +++ b/app/authentication/migrations/0004_auto_20190711_1539.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-07-11 15:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0003_auto_20190711_1054'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=255, null=True, unique=True), + ), + ] diff --git a/app/authentication/migrations/0005_auto_20190711_1828.py b/app/authentication/migrations/0005_auto_20190711_1828.py new file mode 100644 index 0000000..a46d6fb --- /dev/null +++ b/app/authentication/migrations/0005_auto_20190711_1828.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-07-11 18:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0004_auto_20190711_1539'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='password', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/app/authentication/migrations/0006_auto_20190711_1933.py b/app/authentication/migrations/0006_auto_20190711_1933.py new file mode 100644 index 0000000..490927f --- /dev/null +++ b/app/authentication/migrations/0006_auto_20190711_1933.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-07-11 19:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0005_auto_20190711_1828'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='password', + field=models.CharField(default='!1xKZFUMiyDaS7Exy0CurdiaQ1Pp88xxHpN0kw3pb', max_length=255), + ), + ] diff --git a/app/authentication/migrations/0007_auto_20190716_0917.py b/app/authentication/migrations/0007_auto_20190716_0917.py new file mode 100644 index 0000000..1526873 --- /dev/null +++ b/app/authentication/migrations/0007_auto_20190716_0917.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.3 on 2019-07-16 09:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0006_auto_20190711_1933'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='username', + ), + migrations.AddField( + model_name='user', + name='first_name', + field=models.CharField(default='default', max_length=50), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='last_name', + field=models.CharField(default='default', max_length=50), + preserve_default=False, + ), + ] diff --git a/app/authentication/migrations/0008_auto_20190717_1802.py b/app/authentication/migrations/0008_auto_20190717_1802.py new file mode 100644 index 0000000..d37c202 --- /dev/null +++ b/app/authentication/migrations/0008_auto_20190717_1802.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.3 on 2019-07-17 18:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0007_auto_20190716_0917'), + ] + + operations = [ + migrations.AlterModelOptions( + name='user', + options={'ordering': ['-id'], 'verbose_name_plural': 'All Users'}, + ), + migrations.AlterField( + model_name='user', + name='password', + field=models.CharField(default='YouAndM3', max_length=255), + ), + ] diff --git a/app/authentication/migrations/0009_auto_20190722_1140.py b/app/authentication/migrations/0009_auto_20190722_1140.py new file mode 100644 index 0000000..0cfc8f6 --- /dev/null +++ b/app/authentication/migrations/0009_auto_20190722_1140.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-07-22 11:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0008_auto_20190717_1802'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='password', + field=models.CharField(default='!1xKZFUMiyDaS7Exy0CurdiaQ1Pp88xxHpN0kw3pb', max_length=255), + ), + ] diff --git a/app/authentication/migrations/__init__.py b/app/authentication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/authentication/models.py b/app/authentication/models.py new file mode 100644 index 0000000..b7d05e9 --- /dev/null +++ b/app/authentication/models.py @@ -0,0 +1,101 @@ +from datetime import datetime, timedelta + +import jwt +from decouple import config +from django.conf import settings +from django.contrib.auth.models import (AbstractBaseUser, BaseUserManager, + PermissionsMixin) +from django.core.exceptions import ValidationError +from django.db import models +from firebase_admin import auth +from phonenumber_field.modelfields import PhoneNumberField + +from .choices import GENDER_CHOICES + + +class UserManager(BaseUserManager): + use_in_migrations = True + + def createuser(self, **fields): + email = fields.pop("email") + + if not email: + raise ValueError("Email address is required") + email = self.normalize_email(email) + user = self.model(email=email, **fields) + user.save(using=self._db) + return user + + def create_staffuser(self, **fields): + """ + Create and return a `User` with superuser (admin) permissions. + """ + user = self.createuser(**fields) + user.is_staff = True + user.save(using=self._db) + return user + + def create_superuser(self, **fields): + """ + Create and return a `User` with superuser (admin) permissions. + """ + + user = self.createuser(**fields) + user.is_superuser = True + user.is_staff = True + user.save() + + return user + + +class User(AbstractBaseUser, PermissionsMixin): + + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + email = models.EmailField(db_index=True, unique=True) + phone_number = PhoneNumberField(db_index=True, unique=True) + date_of_birth = models.DateField() + gender = models.CharField(max_length=1, choices=GENDER_CHOICES) + password = models.CharField(max_length=255, default=config("P_WORD")) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # The `USERNAME_FIELD` property specifies the log in field. + USERNAME_FIELD = "phone_number" + REQUIRED_FIELDS = ["email", "date_of_birth"] + + # the UserManager class should manage objects of this type. + objects = UserManager() + + class Meta: + verbose_name_plural = "All Users" + ordering = ["-id"] + + def __str__(self): + """ + Returns a string representation of this `User`. + used when a `User` is printed in the console. + """ + return self.email + + # @property + # def token(self): + # """ + # method allows us to get a user's token + # """ + # return self._generate_firebase_token() + + # def _generate_firebase_token(self): + # """ + # Generates a jwt token + # """ + # dt = datetime.now() + timedelta(days=60) + + # token = jwt.encode({ + # 'id': self.pk, + # 'exp': int(dt.strftime('%s')) + # }, settings.SECRET_KEY, algorithm='HS256') + + # return token.decode('utf-8') diff --git a/app/authentication/serializers.py b/app/authentication/serializers.py new file mode 100644 index 0000000..1abfed0 --- /dev/null +++ b/app/authentication/serializers.py @@ -0,0 +1,95 @@ +import json + +from decouple import config +from django.contrib.auth import authenticate +from firebase_admin import auth +from phonenumber_field.serializerfields import PhoneNumberField +from rest_framework import serializers +from rest_framework.validators import UniqueValidator + +from ..auth_backend import PasswordlessAuthBackend +from ..user_profile.models import Profile +from .backends import JWTAuthentication +from .models import User + + +class RegistrationSerializer(serializers.ModelSerializer): + phone_number = PhoneNumberField( + validators=[ + UniqueValidator( + queryset=User.objects.all(), + message="User with this Phone Number already exists.", + ) + ] + ) + full_name = serializers.SerializerMethodField() + + class Meta: + model = User + fields = [ + "id", + "first_name", + "last_name", + "full_name", + "email", + "phone_number", + "date_of_birth", + "gender", + ] + + def get_full_name(self, obj): + return f"{obj.first_name} {obj.last_name}" + + def create(self, validated_data): + return User.objects.createuser(**validated_data) + + +class LoginSerializer(serializers.Serializer): + phone_number = PhoneNumberField() + email = serializers.CharField(max_length=255, read_only=True) + token = serializers.SerializerMethodField() + first_name = serializers.CharField(max_length=50, read_only=True) + last_name = serializers.CharField(max_length=50, read_only=True) + gender = serializers.CharField(max_length=50, read_only=True) + date_of_birth = serializers.CharField(max_length=50, read_only=True) + + def get_token(self, obj): + token = JWTAuthentication.generate_token(self, obj.pk) + return token + + class Meta: + model = User + fields = ["id", "first_name", "last_name", + "phone_number", "token", "gender", "date_of_birth"] + + +class UserSerializer(serializers.ModelSerializer): + """Handles serialization and deserialization of User objects.""" + + class Meta: + model = User + fields = [ + "id", + "first_name", + "last_name", + "phone_number", + "email", + "date_of_birth", + "gender", + ] + + read_only_fields = ("token",) + + def update(self, instance, validated_data): + """Performs an update on a User.""" + + email = validated_data.pop("email") + phone_number = validated_data.pop("phone_number") + + for (key, value) in validated_data.items(): + + setattr(instance, key, value) + + instance.save() + + return instance diff --git a/app/authentication/tests.py b/app/authentication/tests.py new file mode 100644 index 0000000..a0a0615 --- /dev/null +++ b/app/authentication/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Write tests here for the application diff --git a/app/authentication/urls.py b/app/authentication/urls.py new file mode 100644 index 0000000..438cfdd --- /dev/null +++ b/app/authentication/urls.py @@ -0,0 +1,30 @@ +from django.conf.urls import include +from django.urls import path +from rest_framework.routers import DefaultRouter, SimpleRouter + +from ..user_profile.views import UserProfileViewSet +from .views import LoginViewSet, RegistrationViewSet, UpdateUserViewSet + +app_name = "authentication" + + +class OptionalTrailingSlashRouter(SimpleRouter): + def __init_(self, trailing_slash="/?"): + self.trailing_slash = trailing_slash + super().__init__() + + +router = OptionalTrailingSlashRouter() + +# users +router.register("users", RegistrationViewSet, "users") +router.register("profiles", UserProfileViewSet, "profile") +router.register("login", LoginViewSet, "login") +router.register("user", UpdateUserViewSet, "user") + + +urlpatterns = [ + # path("login/", LoginViewSet.as_view({'post': 'post'}), name="login"), +] + +urlpatterns += router.urls diff --git a/app/authentication/views.py b/app/authentication/views.py new file mode 100644 index 0000000..cd1b5c0 --- /dev/null +++ b/app/authentication/views.py @@ -0,0 +1,49 @@ +from rest_framework import status +from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import ( + AllowAny, + IsAdminUser, + IsAuthenticated, + IsAuthenticatedOrReadOnly, +) +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from ..filters import UserFilter +from . import models +from .serializers import LoginSerializer, RegistrationSerializer, UserSerializer + + +class RegistrationViewSet(ModelViewSet): + permission_classes = (AllowAny,) + serializer_class = RegistrationSerializer + queryset = models.User.objects.all() + http_method_names = ["post", "get"] + + +class LoginViewSet(ModelViewSet): + permission_classes = (AllowAny,) + serializer_class = LoginSerializer + queryset = models.User.objects.all() + filterset_class = UserFilter + http_method_names = ["get"] + + DEFAULT_LOGIN_DATA = dict([(field, "") + for field in LoginSerializer.Meta.fields]) + + def list(self, request, *args, **kwargs): + phone_number = request.query_params.get("phone_number", "") + if not phone_number: + data = self.DEFAULT_LOGIN_DATA + else: + user = models.User.objects.get_by_natural_key(phone_number) + data = LoginSerializer(user).data + + return Response(data) + + +class UpdateUserViewSet(ModelViewSet): + permission_classes = (IsAuthenticated,) + serializer_class = UserSerializer + queryset = models.User.objects.all() + http_method_names = ["patch", "get"] diff --git a/app/filters.py b/app/filters.py new file mode 100644 index 0000000..ff08155 --- /dev/null +++ b/app/filters.py @@ -0,0 +1,54 @@ +# Standard Library +import functools +import logging +import operator + +# Third-Party Imports +from django.db.models import Q +from django_filters import rest_framework as filters + +# App Imports +from .authentication.models import User + +logger = logging.getLogger(__name__) + +NULL_VALUE = "unspecified" + + +class BaseFilter(filters.FilterSet): + def filter_contains_with_multiple_query_values(self, queryset, name, value): + options = set(value.split(",")) + null_lookup = {} + if NULL_VALUE in options: + options.remove(NULL_VALUE) + null_lookup = {"__".join([name, "isnull"]): True} + if options: + lookup = functools.reduce( + operator.or_, + {Q(**{"__".join([name, "icontains"]): item}) for item in options}, + ) + else: + lookup = Q(**{}) + + return queryset.filter(Q(lookup | Q(**null_lookup))) + + def filter_exact_with_multiple_query_values(self, queryset, name, value): + options = set(value.split(",")) + null_lookup = {} + if NULL_VALUE in options: + options.remove(NULL_VALUE) + null_lookup = {"__".join([name, "isnull"]): True} + lookup = {"__".join([name, "in"]): options} + return queryset.filter(Q(**lookup) | Q(**null_lookup)) + + +class UserFilter(BaseFilter): + phone_number = filters.CharFilter( + field_name="phone_number", + lookup_expr="iexact", + method="filter_exact_with_multiple_query_values", + ) + + class Meta: + model = User + fields = ["phone_number"] diff --git a/app/firebase_auth.py b/app/firebase_auth.py new file mode 100644 index 0000000..bccfb95 --- /dev/null +++ b/app/firebase_auth.py @@ -0,0 +1,41 @@ +import logging + +from decouple import config +from django.contrib.auth import get_user_model +from firebase_admin import auth, credentials, initialize_app +from rest_framework import exceptions +from rest_framework.authentication import TokenAuthentication + +private_key = config("PRIVATE_KEY").replace("\\n", "\n") +payload = { + "type": "service_account", + "project_id": config("PROJECT_ID"), + "private_key": private_key, + "client_email": config("CLIENT_EMAIL"), + "token_uri": "https://oauth2.googleapis.com/token", +} + +cred = credentials.Certificate(payload) +initialize_app(cred) + +User = get_user_model() +logger = logging.getLogger(__name__) + + +class FirebaseTokenAuthentication(TokenAuthentication): + def authenticate_credentials(self, id_token): + try: + token = auth.verify_id_token(id_token) + except Exception as e: + raise exceptions.AuthenticationFailed() + else: + email = token.get("email") + try: + user = User.objects.get(email=email) + except Exception as e: + logger.error(str(e)) + raise exceptions.AuthenticationFailed("User not found") + + if not user.is_active: + raise exceptions.AuthenticationFailed("User inactive or deleted.") + return (user, token) diff --git a/app/user_profile/__init__.py b/app/user_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user_profile/migrations/0001_initial.py b/app/user_profile/migrations/0001_initial.py new file mode 100644 index 0000000..32644d0 --- /dev/null +++ b/app/user_profile/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.3 on 2019-07-08 13:00 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('first_name', models.CharField(blank=True, max_length=30)), + ('last_name', models.CharField(blank=True, max_length=30)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created'], + }, + ), + ] diff --git a/app/user_profile/migrations/0002_auto_20190716_1039.py b/app/user_profile/migrations/0002_auto_20190716_1039.py new file mode 100644 index 0000000..357ab71 --- /dev/null +++ b/app/user_profile/migrations/0002_auto_20190716_1039.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.3 on 2019-07-16 10:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_profile', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='first_name', + field=models.CharField(max_length=50), + ), + migrations.AlterField( + model_name='profile', + name='last_name', + field=models.CharField(max_length=50), + ), + ] diff --git a/app/user_profile/migrations/0003_auto_20190716_1304.py b/app/user_profile/migrations/0003_auto_20190716_1304.py new file mode 100644 index 0000000..3145763 --- /dev/null +++ b/app/user_profile/migrations/0003_auto_20190716_1304.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.3 on 2019-07-16 13:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_profile', '0002_auto_20190716_1039'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/user_profile/migrations/0004_auto_20190716_1405.py b/app/user_profile/migrations/0004_auto_20190716_1405.py new file mode 100644 index 0000000..1b031a9 --- /dev/null +++ b/app/user_profile/migrations/0004_auto_20190716_1405.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.3 on 2019-07-16 14:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_profile', '0003_auto_20190716_1304'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/user_profile/migrations/0005_auto_20190716_1436.py b/app/user_profile/migrations/0005_auto_20190716_1436.py new file mode 100644 index 0000000..b93628b --- /dev/null +++ b/app/user_profile/migrations/0005_auto_20190716_1436.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.3 on 2019-07-16 14:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_profile', '0004_auto_20190716_1405'), + ] + + operations = [ + migrations.RemoveField( + model_name='profile', + name='first_name', + ), + migrations.RemoveField( + model_name='profile', + name='last_name', + ), + ] diff --git a/app/user_profile/migrations/__init__.py b/app/user_profile/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user_profile/models.py b/app/user_profile/models.py new file mode 100644 index 0000000..67aa2c6 --- /dev/null +++ b/app/user_profile/models.py @@ -0,0 +1,24 @@ +from django.contrib.auth import get_user_model +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver +from django_extensions.db.models import TimeStampedModel + +User = get_user_model() + + +class Profile(TimeStampedModel): + user = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE) + + class Meta: + ordering = ["-created"] + + def __str__(self): + return self.user.email + + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + instance.profile.save() diff --git a/app/user_profile/serializers.py b/app/user_profile/serializers.py new file mode 100644 index 0000000..5ddeebb --- /dev/null +++ b/app/user_profile/serializers.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + +from ..authentication.models import User +from .models import Profile + + +class UserProfileSerializer(serializers.ModelSerializer): + """ + Serializer class for getting user profile + """ + + email = serializers.ReadOnlyField(source="user.email") + + class Meta: + model = Profile + fields = ["id", "email"] diff --git a/app/user_profile/views.py b/app/user_profile/views.py new file mode 100644 index 0000000..6333d67 --- /dev/null +++ b/app/user_profile/views.py @@ -0,0 +1,18 @@ +from rest_framework.permissions import ( + AllowAny, + IsAdminUser, + IsAuthenticated, + IsAuthenticatedOrReadOnly, +) +from rest_framework.viewsets import ModelViewSet + +from ..firebase_auth import FirebaseTokenAuthentication +from .models import Profile +from .serializers import UserProfileSerializer + + +class UserProfileViewSet(ModelViewSet): + permission_classes = (AllowAny,) + serializer_class = UserProfileSerializer + queryset = Profile.objects.all() + http_method_names = ["get", "patch"] diff --git a/dscountr/__init__.py b/dscountr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/settings.py b/dscountr/settings.py similarity index 67% rename from app/settings.py rename to dscountr/settings.py index ed639d0..127d620 100644 --- a/app/settings.py +++ b/dscountr/settings.py @@ -40,11 +40,19 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'app', + 'corsheaders', + 'django_extensions', + 'app.authentication', + 'app.user_profile', + 'phonenumber_field', + "django_filters", ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -52,7 +60,9 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'app.urls' +CORS_ORIGIN_ALLOW_ALL = True + +ROOT_URLCONF = 'dscountr.urls' TEMPLATES = [ { @@ -70,7 +80,7 @@ }, ] -WSGI_APPLICATION = 'app.wsgi.application' +WSGI_APPLICATION = 'dscountr.wsgi.application' # Database @@ -80,24 +90,37 @@ 'default': dj_database_url.config() } +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', +) # Password validation # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] +# AUTH_PASSWORD_VALIDATORS = [ +# { +# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', +# }, +# { +# 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', +# }, +# { +# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', +# }, +# { +# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', +# }, +# ] + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + # 'app.firebase_auth.FirebaseTokenAuthentication', + 'app.authentication.backends.JWTAuthentication', + 'rest_framework.authentication.SessionAuthentication', + + ), + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), +} # Internationalization @@ -119,5 +142,9 @@ STATIC_URL = '/static/' +AUTH_USER_MODEL = 'authentication.User' + # Activate Django-Heroku. django_heroku.settings(locals()) + +del DATABASES['default']['OPTIONS']['sslmode'] diff --git a/app/urls.py b/dscountr/urls.py similarity index 84% rename from app/urls.py rename to dscountr/urls.py index 5348901..6109471 100644 --- a/app/urls.py +++ b/dscountr/urls.py @@ -14,8 +14,10 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include +from app.authentication import urls urlpatterns = [ path('admin/', admin.site.urls), + path('api/', include(urls, namespace='authentication')), ] diff --git a/app/wsgi.py b/dscountr/wsgi.py similarity index 82% rename from app/wsgi.py rename to dscountr/wsgi.py index c618da6..c759d4e 100644 --- a/app/wsgi.py +++ b/dscountr/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dscountr.settings') application = get_wsgi_application() diff --git a/manage.py b/manage.py index 648715e..215fac0 100755 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dscountr.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: