├── .env.example ├── .flaskenv ├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── __init__.py ├── api │ ├── auth_routes.py │ └── user_routes.py ├── config.py ├── dev.db ├── forms │ ├── __init__.py │ ├── login_form.py │ └── signup_form.py ├── models │ ├── __init__.py │ ├── db.py │ └── user.py └── seeds │ ├── __init__.py │ └── users.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ └── 20201120_150602_create_users_table.py ├── react-app ├── .env.example ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── components │ ├── LoginFormModal │ │ ├── LoginForm.css │ │ └── index.js │ ├── LoginFormPage │ │ ├── LoginForm.css │ │ └── index.js │ ├── Navigation │ │ ├── Navigation.css │ │ ├── ProfileButton.js │ │ └── index.js │ ├── OpenModalButton │ │ └── index.js │ ├── SignupFormModal │ │ ├── SignupForm.css │ │ └── index.js │ ├── SignupFormPage │ │ ├── SignupForm.css │ │ └── index.js │ └── auth │ │ └── ProtectedRoute.js │ ├── context │ ├── Modal.css │ └── Modal.js │ ├── index.css │ ├── index.js │ └── store │ ├── index.js │ └── session.js └── requirements.txt /.env.example: -------------------------------------------------------------------------------- 1 | # No need for DATABASE_URL to be set if developing from within a devcontainer 2 | 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=sqlite:///dev.db 5 | SCHEMA=flask_schema -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.py[cod] 4 | .venv 5 | .DS_Store 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | click = "==7.1.2" 8 | gunicorn = "==20.1.0" 9 | itsdangerous = "==2.0.1" 10 | python-dotenv = "==0.14.0" 11 | six = "==1.15.0" 12 | Flask = "==2.0.1" 13 | Flask-Cors = "==3.0.8" 14 | Flask-SQLAlchemy = "==2.5.1" 15 | Flask-WTF = "==0.15.1" 16 | Jinja2 = "==3.0.1" 17 | MarkupSafe = "==2.0.1" 18 | SQLAlchemy = "==1.4.19" 19 | Werkzeug = "==2.0.1" 20 | WTForms = "==2.3.3" 21 | Flask-Migrate = "==3.0.1" 22 | Flask-Login = "==0.5.0" 23 | alembic = "==1.6.5" 24 | python-dateutil = "==2.8.1" 25 | python-editor = "==1.0.4" 26 | greenlet = "==1.1.0" 27 | Mako = "==1.1.4" 28 | 29 | [dev-packages] 30 | 31 | 32 | [requires] 33 | python_version = "3.9" 34 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "a9887a4bf85eb7671e0e53de057ccc0600fde3c86edcaf52fd39adc1f33396d5" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:a21fedebb3fb8f6bbbba51a11114f08c78709377051384c9c5ead5705ee93a51", 22 | "sha256:e78be5b919f5bb184e3e0e2dd1ca986f2362e29a2bc933c446fe89f39dbe4e9c" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.6.5" 26 | }, 27 | "click": { 28 | "hashes": [ 29 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 30 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 31 | ], 32 | "index": "pypi", 33 | "version": "==7.1.2" 34 | }, 35 | "flask": { 36 | "hashes": [ 37 | "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", 38 | "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" 39 | ], 40 | "index": "pypi", 41 | "version": "==2.0.1" 42 | }, 43 | "flask-cors": { 44 | "hashes": [ 45 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", 46 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" 47 | ], 48 | "index": "pypi", 49 | "version": "==3.0.8" 50 | }, 51 | "flask-login": { 52 | "hashes": [ 53 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", 54 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" 55 | ], 56 | "index": "pypi", 57 | "version": "==0.5.0" 58 | }, 59 | "flask-migrate": { 60 | "hashes": [ 61 | "sha256:4d42e8f861d78cb6e9319afcba5bf76062e5efd7784184dd2a1cccd9de34a702", 62 | "sha256:df9043d2050df3c0e0f6313f6b529b62c837b6033c20335e9d0b4acdf2c40e23" 63 | ], 64 | "index": "pypi", 65 | "version": "==3.0.1" 66 | }, 67 | "flask-sqlalchemy": { 68 | "hashes": [ 69 | "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", 70 | "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" 71 | ], 72 | "index": "pypi", 73 | "version": "==2.5.1" 74 | }, 75 | "flask-wtf": { 76 | "hashes": [ 77 | "sha256:6ff7af73458f182180906a37a783e290bdc8a3817fe4ad17227563137ca285bf", 78 | "sha256:ff177185f891302dc253437fe63081e7a46a4e99aca61dfe086fb23e54fff2dc" 79 | ], 80 | "index": "pypi", 81 | "version": "==0.15.1" 82 | }, 83 | "greenlet": { 84 | "hashes": [ 85 | "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c", 86 | "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832", 87 | "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08", 88 | "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e", 89 | "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22", 90 | "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f", 91 | "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c", 92 | "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea", 93 | "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8", 94 | "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad", 95 | "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc", 96 | "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16", 97 | "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8", 98 | "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5", 99 | "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99", 100 | "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e", 101 | "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a", 102 | "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56", 103 | "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c", 104 | "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed", 105 | "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959", 106 | "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922", 107 | "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927", 108 | "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e", 109 | "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a", 110 | "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131", 111 | "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919", 112 | "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319", 113 | "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae", 114 | "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535", 115 | "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505", 116 | "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11", 117 | "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47", 118 | "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821", 119 | "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857", 120 | "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da", 121 | "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc", 122 | "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5", 123 | "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb", 124 | "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05", 125 | "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5", 126 | "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee", 127 | "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e", 128 | "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831", 129 | "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f", 130 | "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3", 131 | "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6", 132 | "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3", 133 | "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f" 134 | ], 135 | "index": "pypi", 136 | "version": "==1.1.0" 137 | }, 138 | "gunicorn": { 139 | "hashes": [ 140 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", 141 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" 142 | ], 143 | "index": "pypi", 144 | "version": "==20.1.0" 145 | }, 146 | "itsdangerous": { 147 | "hashes": [ 148 | "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", 149 | "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" 150 | ], 151 | "index": "pypi", 152 | "version": "==2.0.1" 153 | }, 154 | "jinja2": { 155 | "hashes": [ 156 | "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", 157 | "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" 158 | ], 159 | "index": "pypi", 160 | "version": "==3.0.1" 161 | }, 162 | "mako": { 163 | "hashes": [ 164 | "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab", 165 | "sha256:aea166356da44b9b830c8023cd9b557fa856bd8b4035d6de771ca027dfc5cc6e" 166 | ], 167 | "index": "pypi", 168 | "version": "==1.1.4" 169 | }, 170 | "markupsafe": { 171 | "hashes": [ 172 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 173 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 174 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 175 | "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", 176 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 177 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 178 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 179 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 180 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 181 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 182 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 183 | "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", 184 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 185 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 186 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 187 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 188 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 189 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 190 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 191 | "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", 192 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 193 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 194 | "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", 195 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 196 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 197 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 198 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 199 | "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", 200 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 201 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 202 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 203 | "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", 204 | "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", 205 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 206 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 207 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 208 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 209 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 210 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 211 | "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", 212 | "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", 213 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 214 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 215 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 216 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 217 | "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", 218 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 219 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 220 | "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", 221 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 222 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 223 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 224 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 225 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 226 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 227 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 228 | "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", 229 | "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", 230 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 231 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 232 | "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", 233 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 234 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 235 | "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", 236 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 237 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 238 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 239 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 240 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 241 | ], 242 | "index": "pypi", 243 | "version": "==2.0.1" 244 | }, 245 | "python-dateutil": { 246 | "hashes": [ 247 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 248 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 249 | ], 250 | "index": "pypi", 251 | "version": "==2.8.1" 252 | }, 253 | "python-dotenv": { 254 | "hashes": [ 255 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", 256 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" 257 | ], 258 | "index": "pypi", 259 | "version": "==0.14.0" 260 | }, 261 | "python-editor": { 262 | "hashes": [ 263 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 264 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 265 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 266 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 267 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 268 | ], 269 | "index": "pypi", 270 | "version": "==1.0.4" 271 | }, 272 | "six": { 273 | "hashes": [ 274 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 275 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 276 | ], 277 | "index": "pypi", 278 | "version": "==1.15.0" 279 | }, 280 | "sqlalchemy": { 281 | "hashes": [ 282 | "sha256:0fb3f73e5009f5a4c9b24469939d3d57cc3ad8099a09c0cfefc47fe45ab7ffbe", 283 | "sha256:20f4bf1459548a74aade997cb045015e4d72f0fde1789b09b3bb380be28f6511", 284 | "sha256:2ace9ab2af9d7d7b0e2ff2178809941c56ab8921e38128278192a73a8a1c08a2", 285 | "sha256:311051c06f905774427b4a92dcb3924d6ee563dea3a88176da02fdfc572d0d1d", 286 | "sha256:45b0f773e195d8d51e2fd67cb5b5fb32f5a1f5e7f0752016207091bed108909a", 287 | "sha256:57ba8a96b6d058c7dcf44de8ac0955b7a787f7177a0221dd4b8016e0191268f5", 288 | "sha256:58d4f79d119010fdced6e7fd7e4b9f2230dbf55a8235d7c58b1c8207ef74791b", 289 | "sha256:5c92d9ebf4b38c22c0c9e4f203a80e101910a50dc555b4578816932015b97d7f", 290 | "sha256:6317701c06a829b066c794545512bb70b1a10a74574cfa5658a0aaf49f31aa93", 291 | "sha256:64eab458619ef759f16f0f82242813d3289e829f8557fbc7c212ca4eadf96472", 292 | "sha256:6fd1b745ade2020a1a7bf1e22536d8afe86287882c81ca5d860bdf231d5854e9", 293 | "sha256:89a5a13dcf33b7e47c7a9404a297c836965a247c7f076a0fe0910cae2bee5ce2", 294 | "sha256:8cba69545246d16c6d2a12ce45865947cbdd814bacddf2e532fdd4512e70728c", 295 | "sha256:8f1e7f4de05c15d6b46af12f3cf0c2552f2940d201a49926703249a62402d851", 296 | "sha256:9014fd1d8aebcb4eb6bc69a382dd149200e1d5924412b1d08b4443f6c1ce526f", 297 | "sha256:9133635edcec1e7fbfc16eba5dc2b5b3b11818d25b7a57cfcbfa8d3b3e9594fd", 298 | "sha256:93ba458b3c279581288a10a55df2aa6ac3509882228fcbad9d9d88069f899337", 299 | "sha256:942ca49b7ec7449d2473a6587825c55ad99534ddfc4eee249dd42be3cc1aa8c9", 300 | "sha256:95a9fd0a11f89a80d8815418eccba034f3fec8ea1f04c41b6b8decc5c95852e9", 301 | "sha256:96d3d4a7ead376d738775a1fa9786dc17a31975ec664cea284e53735c79a5686", 302 | "sha256:9c0945c79cbe507b49524e31a4bb8700060bbccb60bb553df6432e176baff3d5", 303 | "sha256:a34a7fd3353ee61a1dca72fc0c3e38d4e56bdc2c343e712f60a8c70acd4ef5bf", 304 | "sha256:c6efc7477551ba9ce632d5c3b448b7de0277c86005eec190a1068fcc7115fd0e", 305 | "sha256:cefd44faca7c57534503261f6fab49bd47eb9c2945ee0bab09faaa8cb047c24f", 306 | "sha256:d04160462f874eaa4d88721a0d5ecca8ebf433616801efe779f252ef87b0e216", 307 | "sha256:d3cf5f543d048a7c8da500133068c5c90c97a2c4bf0c027928a85028a519f33d", 308 | "sha256:d7b21a4b62921cf6dca97e8f9dea1fbe2432aebbb09895a2bd4f527105af41a4", 309 | "sha256:ddbce8fe4d0190db21db602e38aaf4c158c540b49f1ef7475323ec682a9fbf2d", 310 | "sha256:e2761b925fda550debfd5a8bc3cef9debc9a23c6a280429c4ec3a07c35c6b4b3", 311 | "sha256:fa05a77662c23226c9ec031638fd90ae767009e05cd092b948740f09d10645f0" 312 | ], 313 | "index": "pypi", 314 | "version": "==1.4.19" 315 | }, 316 | "werkzeug": { 317 | "hashes": [ 318 | "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", 319 | "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" 320 | ], 321 | "index": "pypi", 322 | "version": "==2.0.1" 323 | }, 324 | "wtforms": { 325 | "hashes": [ 326 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c", 327 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c" 328 | ], 329 | "index": "pypi", 330 | "version": "==2.3.3" 331 | } 332 | }, 333 | "develop": {} 334 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask React Project 2 | 3 | This is the starter for the Flask React project. 4 | 5 | ## Getting started 6 | 1. Clone this repository (only this branch) 7 | 8 | 2. Install dependencies 9 | 10 | ```bash 11 | pipenv install -r requirements.txt 12 | ``` 13 | 14 | 3. Create a **.env** file based on the example with proper settings for your 15 | development environment 16 | 17 | 4. Make sure the SQLite3 database connection URL is in the **.env** file 18 | 19 | 5. This starter organizes all tables inside the `flask_schema` schema, defined 20 | by the `SCHEMA` environment variable. Replace the value for 21 | `SCHEMA` with a unique name, **making sure you use the snake_case 22 | convention**. 23 | 24 | 6. Get into your pipenv, migrate your database, seed your database, and run your Flask app 25 | 26 | ```bash 27 | pipenv shell 28 | ``` 29 | 30 | ```bash 31 | flask db upgrade 32 | ``` 33 | 34 | ```bash 35 | flask seed all 36 | ``` 37 | 38 | ```bash 39 | flask run 40 | ``` 41 | 42 | 7. To run the React App in development, checkout the [README](./react-app/README.md) inside the `react-app` directory. 43 | 44 | 45 | ## Deployment through Render.com 46 | 47 | First, refer to your Render.com deployment articles for more detailed 48 | instructions about getting started with [Render.com], creating a production 49 | database, and deployment debugging tips. 50 | 51 | From the [Dashboard], click on the "New +" button in the navigation bar, and 52 | click on "Web Service" to create the application that will be deployed. 53 | 54 | Look for the name of the application you want to deploy, and click the "Connect" 55 | button to the right of the name. 56 | 57 | Now, fill out the form to configure the build and start commands, as well as add 58 | the environment variables to properly deploy the application. 59 | 60 | ### Part A: Configure the Start and Build Commands 61 | 62 | Start by giving your application a name. 63 | 64 | Leave the root directory field blank. By default, Render will run commands from 65 | the root directory. 66 | 67 | Make sure the Environment field is set set to "Python 3", the Region is set to 68 | the location closest to you, and the Branch is set to "main". 69 | 70 | Next, add your Build command. This is a script that should include everything 71 | that needs to happen _before_ starting the server. 72 | 73 | For your Flask project, enter the following command into the Build field, all in 74 | one line: 75 | 76 | ```shell 77 | # build command - enter all in one line 78 | npm install --prefix react-app && 79 | npm run build --prefix react-app && 80 | pip install -r requirements.txt && 81 | pip install psycopg2 && 82 | flask db upgrade && 83 | flask seed all 84 | ``` 85 | 86 | This script will install dependencies for the frontend, and run the build 87 | command in the __package.json__ file for the frontend, which builds the React 88 | application. Then, it will install the dependencies needed for the Python 89 | backend, and run the migration and seed files. 90 | 91 | Now, add your start command in the Start field: 92 | 93 | ```shell 94 | # start script 95 | gunicorn app:app 96 | ``` 97 | 98 | _If you are using websockets, use the following start command instead for increased performance:_ 99 | 100 | `gunicorn --worker-class eventlet -w 1 app:app` 101 | 102 | ### Part B: Add the Environment Variables 103 | 104 | Click on the "Advanced" button at the bottom of the form to configure the 105 | environment variables your application needs to access to run properly. In the 106 | development environment, you have been securing these variables in the __.env__ 107 | file, which has been removed from source control. In this step, you will need to 108 | input the keys and values for the environment variables you need for production 109 | into the Render GUI. 110 | 111 | Click on "Add Environment Variable" to start adding all of the variables you 112 | need for the production environment. 113 | 114 | Add the following keys and values in the Render GUI form: 115 | 116 | - SECRET_KEY (click "Generate" to generate a secure secret for production) 117 | - FLASK_ENV production 118 | - FLASK_APP app 119 | - SCHEMA (your unique schema name, in snake_case) 120 | - REACT_APP_BASE_URL (use render.com url, located at top of page, similar to 121 | https://this-application-name.onrender.com) 122 | 123 | In a new tab, navigate to your dashboard and click on your Postgres database 124 | instance. 125 | 126 | Add the following keys and values: 127 | 128 | - DATABASE_URL (copy value from Internal Database URL field) 129 | 130 | _Note: Add any other keys and values that may be present in your local __.env__ 131 | file. As you work to further develop your project, you may need to add more 132 | environment variables to your local __.env__ file. Make sure you add these 133 | environment variables to the Render GUI as well for the next deployment._ 134 | 135 | Next, choose "Yes" for the Auto-Deploy field. This will re-deploy your 136 | application every time you push to main. 137 | 138 | Now, you are finally ready to deploy! Click "Create Web Service" to deploy your 139 | project. The deployment process will likely take about 10-15 minutes if 140 | everything works as expected. You can monitor the logs to see your build and 141 | start commands being executed, and see any errors in the build process. 142 | 143 | When deployment is complete, open your deployed site and check to see if you 144 | successfully deployed your Flask application to Render! You can find the URL for 145 | your site just below the name of the Web Service at the top of the page. 146 | 147 | [Render.com]: https://render.com/ 148 | [Dashboard]: https://dashboard.render.com/ -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template, request, session, redirect 3 | from flask_cors import CORS 4 | from flask_migrate import Migrate 5 | from flask_wtf.csrf import CSRFProtect, generate_csrf 6 | from flask_login import LoginManager 7 | from .models import db, User 8 | from .api.user_routes import user_routes 9 | from .api.auth_routes import auth_routes 10 | from .seeds import seed_commands 11 | from .config import Config 12 | 13 | app = Flask(__name__, static_folder='../react-app/build', static_url_path='/') 14 | 15 | # Setup login manager 16 | login = LoginManager(app) 17 | login.login_view = 'auth.unauthorized' 18 | 19 | 20 | @login.user_loader 21 | def load_user(id): 22 | return User.query.get(int(id)) 23 | 24 | 25 | # Tell flask about our seed commands 26 | app.cli.add_command(seed_commands) 27 | 28 | app.config.from_object(Config) 29 | app.register_blueprint(user_routes, url_prefix='/api/users') 30 | app.register_blueprint(auth_routes, url_prefix='/api/auth') 31 | db.init_app(app) 32 | Migrate(app, db) 33 | 34 | # Application Security 35 | CORS(app) 36 | 37 | 38 | # Since we are deploying with Docker and Flask, 39 | # we won't be using a buildpack when we deploy to Heroku. 40 | # Therefore, we need to make sure that in production any 41 | # request made over http is redirected to https. 42 | # Well......... 43 | @app.before_request 44 | def https_redirect(): 45 | if os.environ.get('FLASK_ENV') == 'production': 46 | if request.headers.get('X-Forwarded-Proto') == 'http': 47 | url = request.url.replace('http://', 'https://', 1) 48 | code = 301 49 | return redirect(url, code=code) 50 | 51 | 52 | @app.after_request 53 | def inject_csrf_token(response): 54 | response.set_cookie( 55 | 'csrf_token', 56 | generate_csrf(), 57 | secure=True if os.environ.get('FLASK_ENV') == 'production' else False, 58 | samesite='Strict' if os.environ.get( 59 | 'FLASK_ENV') == 'production' else None, 60 | httponly=True) 61 | return response 62 | 63 | 64 | @app.route("/api/docs") 65 | def api_help(): 66 | """ 67 | Returns all API routes and their doc strings 68 | """ 69 | acceptable_methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] 70 | route_list = { rule.rule: [[ method for method in rule.methods if method in acceptable_methods ], 71 | app.view_functions[rule.endpoint].__doc__ ] 72 | for rule in app.url_map.iter_rules() if rule.endpoint != 'static' } 73 | return route_list 74 | 75 | 76 | @app.route('/', defaults={'path': ''}) 77 | @app.route('/') 78 | def react_root(path): 79 | """ 80 | This route will direct to the public directory in our 81 | react builds in the production environment for favicon 82 | or index.html requests 83 | """ 84 | if path == 'favicon.ico': 85 | return app.send_from_directory('public', 'favicon.ico') 86 | return app.send_static_file('index.html') 87 | 88 | 89 | @app.errorhandler(404) 90 | def not_found(e): 91 | return app.send_static_file('index.html') -------------------------------------------------------------------------------- /app/api/auth_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, session, request 2 | from app.models import User, db 3 | from app.forms import LoginForm 4 | from app.forms import SignUpForm 5 | from flask_login import current_user, login_user, logout_user, login_required 6 | 7 | auth_routes = Blueprint('auth', __name__) 8 | 9 | 10 | def validation_errors_to_error_messages(validation_errors): 11 | """ 12 | Simple function that turns the WTForms validation errors into a simple list 13 | """ 14 | errorMessages = [] 15 | for field in validation_errors: 16 | for error in validation_errors[field]: 17 | errorMessages.append(f'{field} : {error}') 18 | return errorMessages 19 | 20 | 21 | @auth_routes.route('/') 22 | def authenticate(): 23 | """ 24 | Authenticates a user. 25 | """ 26 | if current_user.is_authenticated: 27 | return current_user.to_dict() 28 | return {'errors': ['Unauthorized']} 29 | 30 | 31 | @auth_routes.route('/login', methods=['POST']) 32 | def login(): 33 | """ 34 | Logs a user in 35 | """ 36 | form = LoginForm() 37 | # Get the csrf_token from the request cookie and put it into the 38 | # form manually to validate_on_submit can be used 39 | form['csrf_token'].data = request.cookies['csrf_token'] 40 | if form.validate_on_submit(): 41 | # Add the user to the session, we are logged in! 42 | user = User.query.filter(User.email == form.data['email']).first() 43 | login_user(user) 44 | return user.to_dict() 45 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 46 | 47 | 48 | @auth_routes.route('/logout') 49 | def logout(): 50 | """ 51 | Logs a user out 52 | """ 53 | logout_user() 54 | return {'message': 'User logged out'} 55 | 56 | 57 | @auth_routes.route('/signup', methods=['POST']) 58 | def sign_up(): 59 | """ 60 | Creates a new user and logs them in 61 | """ 62 | form = SignUpForm() 63 | form['csrf_token'].data = request.cookies['csrf_token'] 64 | if form.validate_on_submit(): 65 | user = User( 66 | username=form.data['username'], 67 | email=form.data['email'], 68 | password=form.data['password'] 69 | ) 70 | db.session.add(user) 71 | db.session.commit() 72 | login_user(user) 73 | return user.to_dict() 74 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 75 | 76 | 77 | @auth_routes.route('/unauthorized') 78 | def unauthorized(): 79 | """ 80 | Returns unauthorized JSON when flask-login authentication fails 81 | """ 82 | return {'errors': ['Unauthorized']}, 401 -------------------------------------------------------------------------------- /app/api/user_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask_login import login_required 3 | from app.models import User 4 | 5 | user_routes = Blueprint('users', __name__) 6 | 7 | 8 | @user_routes.route('/') 9 | @login_required 10 | def users(): 11 | """ 12 | Query for all users and returns them in a list of user dictionaries 13 | """ 14 | users = User.query.all() 15 | return {'users': [user.to_dict() for user in users]} 16 | 17 | 18 | @user_routes.route('/') 19 | @login_required 20 | def user(id): 21 | """ 22 | Query for a user by id and returns that user in a dictionary 23 | """ 24 | user = User.query.get(id) 25 | return user.to_dict() -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | SECRET_KEY = os.environ.get('SECRET_KEY') 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | # SQLAlchemy 1.4 no longer supports url strings that start with 'postgres' 8 | # (only 'postgresql') but heroku's postgres add-on automatically sets the 9 | # url in the hidden config vars to start with postgres. 10 | # so the connection uri must be updated here (for production) 11 | SQLALCHEMY_DATABASE_URI = os.environ.get( 12 | 'DATABASE_URL').replace('postgres://', 'postgresql://') 13 | SQLALCHEMY_ECHO = True -------------------------------------------------------------------------------- /app/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appacademy/practice-for-week-19-python-project-skeleton/a4d103956d7174d37a4b35db55a6281f197712f5/app/dev.db -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm 3 | -------------------------------------------------------------------------------- /app/forms/login_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | from app.models import User 5 | 6 | 7 | def user_exists(form, field): 8 | # Checking if user exists 9 | email = field.data 10 | user = User.query.filter(User.email == email).first() 11 | if not user: 12 | raise ValidationError('Email provided not found.') 13 | 14 | 15 | def password_matches(form, field): 16 | # Checking if password matches 17 | password = field.data 18 | email = form.data['email'] 19 | user = User.query.filter(User.email == email).first() 20 | if not user: 21 | raise ValidationError('No such user exists.') 22 | if not user.check_password(password): 23 | raise ValidationError('Password was incorrect.') 24 | 25 | 26 | class LoginForm(FlaskForm): 27 | email = StringField('email', validators=[DataRequired(), user_exists]) 28 | password = StringField('password', validators=[DataRequired(), password_matches]) -------------------------------------------------------------------------------- /app/forms/signup_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | from app.models import User 5 | 6 | 7 | def user_exists(form, field): 8 | # Checking if user exists 9 | email = field.data 10 | user = User.query.filter(User.email == email).first() 11 | if user: 12 | raise ValidationError('Email address is already in use.') 13 | 14 | 15 | def username_exists(form, field): 16 | # Checking if username is already in use 17 | username = field.data 18 | user = User.query.filter(User.username == username).first() 19 | if user: 20 | raise ValidationError('Username is already in use.') 21 | 22 | 23 | class SignUpForm(FlaskForm): 24 | username = StringField( 25 | 'username', validators=[DataRequired(), username_exists]) 26 | email = StringField('email', validators=[DataRequired(), user_exists]) 27 | password = StringField('password', validators=[DataRequired()]) -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | from .db import environment, SCHEMA 4 | -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | import os 4 | environment = os.getenv("FLASK_ENV") 5 | SCHEMA = os.environ.get("SCHEMA") 6 | 7 | 8 | db = SQLAlchemy() 9 | 10 | # helper function for adding prefix to foreign key column references in production 11 | def add_prefix_for_prod(attr): 12 | if environment == "production": 13 | return f"{SCHEMA}.{attr}" 14 | else: 15 | return attr -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from .db import db, environment, SCHEMA, add_prefix_for_prod 2 | from werkzeug.security import generate_password_hash, check_password_hash 3 | from flask_login import UserMixin 4 | 5 | 6 | class User(db.Model, UserMixin): 7 | __tablename__ = 'users' 8 | 9 | if environment == "production": 10 | __table_args__ = {'schema': SCHEMA} 11 | 12 | id = db.Column(db.Integer, primary_key=True) 13 | username = db.Column(db.String(40), nullable=False, unique=True) 14 | email = db.Column(db.String(255), nullable=False, unique=True) 15 | hashed_password = db.Column(db.String(255), nullable=False) 16 | 17 | @property 18 | def password(self): 19 | return self.hashed_password 20 | 21 | @password.setter 22 | def password(self, password): 23 | self.hashed_password = generate_password_hash(password) 24 | 25 | def check_password(self, password): 26 | return check_password_hash(self.password, password) 27 | 28 | def to_dict(self): 29 | return { 30 | 'id': self.id, 31 | 'username': self.username, 32 | 'email': self.email 33 | } 34 | -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .users import seed_users, undo_users 3 | 4 | from app.models.db import db, environment, SCHEMA 5 | 6 | # Creates a seed group to hold our commands 7 | # So we can type `flask seed --help` 8 | seed_commands = AppGroup('seed') 9 | 10 | 11 | # Creates the `flask seed all` command 12 | @seed_commands.command('all') 13 | def seed(): 14 | if environment == 'production': 15 | # Before seeding in production, you want to run the seed undo 16 | # command, which will truncate all tables prefixed with 17 | # the schema name (see comment in users.py undo_users function). 18 | # Make sure to add all your other model's undo functions below 19 | undo_users() 20 | seed_users() 21 | # Add other seed functions here 22 | 23 | 24 | # Creates the `flask seed undo` command 25 | @seed_commands.command('undo') 26 | def undo(): 27 | undo_users() 28 | # Add other undo functions here -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | from app.models import db, User, environment, SCHEMA 2 | from sqlalchemy.sql import text 3 | 4 | 5 | # Adds a demo user, you can add other users here if you want 6 | def seed_users(): 7 | demo = User( 8 | username='Demo', email='demo@aa.io', password='password') 9 | marnie = User( 10 | username='marnie', email='marnie@aa.io', password='password') 11 | bobbie = User( 12 | username='bobbie', email='bobbie@aa.io', password='password') 13 | 14 | db.session.add(demo) 15 | db.session.add(marnie) 16 | db.session.add(bobbie) 17 | db.session.commit() 18 | 19 | 20 | # Uses a raw SQL query to TRUNCATE or DELETE the users table. SQLAlchemy doesn't 21 | # have a built in function to do this. With postgres in production TRUNCATE 22 | # removes all the data from the table, and RESET IDENTITY resets the auto 23 | # incrementing primary key, CASCADE deletes any dependent entities. With 24 | # sqlite3 in development you need to instead use DELETE to remove all data and 25 | # it will reset the primary keys for you as well. 26 | def undo_users(): 27 | if environment == "production": 28 | db.session.execute(f"TRUNCATE table {SCHEMA}.users RESTART IDENTITY CASCADE;") 29 | else: 30 | db.session.execute(text("DELETE FROM users")) 31 | 32 | db.session.commit() -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | import os 12 | environment = os.getenv("FLASK_ENV") 13 | SCHEMA = os.environ.get("SCHEMA") 14 | 15 | 16 | # this is the Alembic Config object, which provides 17 | # access to the values within the .ini file in use. 18 | config = context.config 19 | 20 | # Interpret the config file for Python logging. 21 | # This line sets up loggers basically. 22 | fileConfig(config.config_file_name) 23 | logger = logging.getLogger('alembic.env') 24 | 25 | # add your model's MetaData object here 26 | # for 'autogenerate' support 27 | # from myapp import mymodel 28 | # target_metadata = mymodel.Base.metadata 29 | from flask import current_app 30 | config.set_main_option( 31 | 'sqlalchemy.url', 32 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) 33 | target_metadata = current_app.extensions['migrate'].db.metadata 34 | 35 | # other values from the config, defined by the needs of env.py, 36 | # can be acquired: 37 | # my_important_option = config.get_main_option("my_important_option") 38 | # ... etc. 39 | 40 | 41 | def run_migrations_offline(): 42 | """Run migrations in 'offline' mode. 43 | 44 | This configures the context with just a URL 45 | and not an Engine, though an Engine is acceptable 46 | here as well. By skipping the Engine creation 47 | we don't even need a DBAPI to be available. 48 | 49 | Calls to context.execute() here emit the given string to the 50 | script output. 51 | 52 | """ 53 | url = config.get_main_option("sqlalchemy.url") 54 | context.configure( 55 | url=url, target_metadata=target_metadata, literal_binds=True 56 | ) 57 | 58 | with context.begin_transaction(): 59 | context.run_migrations() 60 | 61 | 62 | def run_migrations_online(): 63 | """Run migrations in 'online' mode. 64 | 65 | In this scenario we need to create an Engine 66 | and associate a connection with the context. 67 | 68 | """ 69 | 70 | # this callback is used to prevent an auto-migration from being generated 71 | # when there are no changes to the schema 72 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 73 | def process_revision_directives(context, revision, directives): 74 | if getattr(config.cmd_opts, 'autogenerate', False): 75 | script = directives[0] 76 | if script.upgrade_ops.is_empty(): 77 | directives[:] = [] 78 | logger.info('No changes in schema detected.') 79 | 80 | connectable = engine_from_config( 81 | config.get_section(config.config_ini_section), 82 | prefix='sqlalchemy.', 83 | poolclass=pool.NullPool, 84 | ) 85 | 86 | with connectable.connect() as connection: 87 | context.configure( 88 | connection=connection, 89 | target_metadata=target_metadata, 90 | process_revision_directives=process_revision_directives, 91 | **current_app.extensions['migrate'].configure_args 92 | ) 93 | # Create a schema (only in production) 94 | if environment == "production": 95 | connection.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}") 96 | 97 | # Set search path to your schema (only in production) 98 | with context.begin_transaction(): 99 | if environment == "production": 100 | context.execute(f"SET search_path TO {SCHEMA}") 101 | context.run_migrations() 102 | 103 | if context.is_offline_mode(): 104 | run_migrations_offline() 105 | else: 106 | run_migrations_online() -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} -------------------------------------------------------------------------------- /migrations/versions/20201120_150602_create_users_table.py: -------------------------------------------------------------------------------- 1 | """create_users_table 2 | 3 | Revision ID: ffdc0a98111c 4 | Revises: 5 | Create Date: 2020-11-20 15:06:02.230689 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | import os 12 | environment = os.getenv("FLASK_ENV") 13 | SCHEMA = os.environ.get("SCHEMA") 14 | 15 | 16 | # revision identifiers, used by Alembic. 17 | revision = 'ffdc0a98111c' 18 | down_revision = None 19 | branch_labels = None 20 | depends_on = None 21 | 22 | 23 | def upgrade(): 24 | # ### commands auto generated by Alembic - please adjust! ### 25 | op.create_table('users', 26 | sa.Column('id', sa.Integer(), nullable=False), 27 | sa.Column('username', sa.String(length=40), nullable=False), 28 | sa.Column('email', sa.String(length=255), nullable=False), 29 | sa.Column('hashed_password', sa.String(length=255), nullable=False), 30 | sa.PrimaryKeyConstraint('id'), 31 | sa.UniqueConstraint('email'), 32 | sa.UniqueConstraint('username') 33 | ) 34 | 35 | if environment == "production": 36 | op.execute(f"ALTER TABLE users SET SCHEMA {SCHEMA};") 37 | # ### end Alembic commands ###qqqqqqqqq 38 | 39 | 40 | def downgrade(): 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_table('users') 43 | # ### end Alembic commands ### -------------------------------------------------------------------------------- /react-app/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:5000 2 | -------------------------------------------------------------------------------- /react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* -------------------------------------------------------------------------------- /react-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | Your React App will live here. You will need to run `npm install` to install all your dependencies before starting up the application. While in development, run this application from this location using `npm start`. 4 | 5 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable when you deploy! 6 | 7 | This app will be automatically built when you push to your main branch on Github. 8 | -------------------------------------------------------------------------------- /react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "http-proxy-middleware": "^1.0.5", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-redux": "^7.2.4", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "^4.0.3", 15 | "redux": "^4.1.0", 16 | "redux-logger": "^3.0.6", 17 | "redux-thunk": "^2.3.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "CI=false && react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.3%", 31 | "not ie 11", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "proxy": "http://localhost:5000" 42 | } -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appacademy/practice-for-week-19-python-project-skeleton/a4d103956d7174d37a4b35db55a6281f197712f5/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flask Starter 6 | 7 | 8 | 9 | 11 | 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Authenticate Me", 3 | "name": "Authenticate Me App Academy Project", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { Route, Switch } from "react-router-dom"; 4 | import SignupFormPage from "./components/SignupFormPage"; 5 | import LoginFormPage from "./components/LoginFormPage"; 6 | import { authenticate } from "./store/session"; 7 | import Navigation from "./components/Navigation"; 8 | 9 | function App() { 10 | const dispatch = useDispatch(); 11 | const [isLoaded, setIsLoaded] = useState(false); 12 | useEffect(() => { 13 | dispatch(authenticate()).then(() => setIsLoaded(true)); 14 | }, [dispatch]); 15 | 16 | return ( 17 | <> 18 | 19 | {isLoaded && ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | )} 29 | 30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /react-app/src/components/LoginFormModal/LoginForm.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appacademy/practice-for-week-19-python-project-skeleton/a4d103956d7174d37a4b35db55a6281f197712f5/react-app/src/components/LoginFormModal/LoginForm.css -------------------------------------------------------------------------------- /react-app/src/components/LoginFormModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { login } from "../../store/session"; 3 | import { useDispatch } from "react-redux"; 4 | import { useModal } from "../../context/Modal"; 5 | import "./LoginForm.css"; 6 | 7 | function LoginFormModal() { 8 | const dispatch = useDispatch(); 9 | const [email, setEmail] = useState(""); 10 | const [password, setPassword] = useState(""); 11 | const [errors, setErrors] = useState([]); 12 | const { closeModal } = useModal(); 13 | 14 | const handleSubmit = async (e) => { 15 | e.preventDefault(); 16 | const data = await dispatch(login(email, password)); 17 | if (data) { 18 | setErrors(data); 19 | } else { 20 | closeModal() 21 | } 22 | }; 23 | 24 | return ( 25 | <> 26 |

Log In

27 |
28 |
    29 | {errors.map((error, idx) => ( 30 |
  • {error}
  • 31 | ))} 32 |
33 | 42 | 51 | 52 |
53 | 54 | ); 55 | } 56 | 57 | export default LoginFormModal; 58 | -------------------------------------------------------------------------------- /react-app/src/components/LoginFormPage/LoginForm.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appacademy/practice-for-week-19-python-project-skeleton/a4d103956d7174d37a4b35db55a6281f197712f5/react-app/src/components/LoginFormPage/LoginForm.css -------------------------------------------------------------------------------- /react-app/src/components/LoginFormPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { login } from "../../store/session"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { Redirect } from "react-router-dom"; 5 | import './LoginForm.css'; 6 | 7 | function LoginFormPage() { 8 | const dispatch = useDispatch(); 9 | const sessionUser = useSelector((state) => state.session.user); 10 | const [email, setEmail] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const [errors, setErrors] = useState([]); 13 | 14 | if (sessionUser) return ; 15 | 16 | const handleSubmit = async (e) => { 17 | e.preventDefault(); 18 | const data = await dispatch(login(email, password)); 19 | if (data) { 20 | setErrors(data); 21 | } 22 | }; 23 | 24 | return ( 25 | <> 26 |

Log In

27 |
28 |
    29 | {errors.map((error, idx) => ( 30 |
  • {error}
  • 31 | ))} 32 |
33 | 42 | 51 | 52 |
53 | 54 | ); 55 | } 56 | 57 | export default LoginFormPage; 58 | -------------------------------------------------------------------------------- /react-app/src/components/Navigation/Navigation.css: -------------------------------------------------------------------------------- 1 | .profile-dropdown { 2 | position: absolute; 3 | } 4 | 5 | .hidden { 6 | display: none; 7 | } -------------------------------------------------------------------------------- /react-app/src/components/Navigation/ProfileButton.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { logout } from "../../store/session"; 4 | import OpenModalButton from "../OpenModalButton"; 5 | import LoginFormModal from "../LoginFormModal"; 6 | import SignupFormModal from "../SignupFormModal"; 7 | 8 | function ProfileButton({ user }) { 9 | const dispatch = useDispatch(); 10 | const [showMenu, setShowMenu] = useState(false); 11 | const ulRef = useRef(); 12 | 13 | const openMenu = () => { 14 | if (showMenu) return; 15 | setShowMenu(true); 16 | }; 17 | 18 | useEffect(() => { 19 | if (!showMenu) return; 20 | 21 | const closeMenu = (e) => { 22 | if (!ulRef.current.contains(e.target)) { 23 | setShowMenu(false); 24 | } 25 | }; 26 | 27 | document.addEventListener("click", closeMenu); 28 | 29 | return () => document.removeEventListener("click", closeMenu); 30 | }, [showMenu]); 31 | 32 | const handleLogout = (e) => { 33 | e.preventDefault(); 34 | dispatch(logout()); 35 | }; 36 | 37 | const ulClassName = "profile-dropdown" + (showMenu ? "" : " hidden"); 38 | const closeMenu = () => setShowMenu(false); 39 | 40 | return ( 41 | <> 42 | 45 |
    46 | {user ? ( 47 | <> 48 |
  • {user.username}
  • 49 |
  • {user.email}
  • 50 |
  • 51 | 52 |
  • 53 | 54 | ) : ( 55 | <> 56 | } 60 | /> 61 | 62 | } 66 | /> 67 | 68 | )} 69 |
70 | 71 | ); 72 | } 73 | 74 | export default ProfileButton; 75 | -------------------------------------------------------------------------------- /react-app/src/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { useSelector } from 'react-redux'; 4 | import ProfileButton from './ProfileButton'; 5 | import './Navigation.css'; 6 | 7 | function Navigation({ isLoaded }){ 8 | const sessionUser = useSelector(state => state.session.user); 9 | 10 | return ( 11 |
    12 |
  • 13 | Home 14 |
  • 15 | {isLoaded && ( 16 |
  • 17 | 18 |
  • 19 | )} 20 |
21 | ); 22 | } 23 | 24 | export default Navigation; -------------------------------------------------------------------------------- /react-app/src/components/OpenModalButton/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useModal } from '../../context/Modal'; 3 | 4 | function OpenModalButton({ 5 | modalComponent, // component to render inside the modal 6 | buttonText, // text of the button that opens the modal 7 | onButtonClick, // optional: callback function that will be called once the button that opens the modal is clicked 8 | onModalClose // optional: callback function that will be called once the modal is closed 9 | }) { 10 | const { setModalContent, setOnModalClose } = useModal(); 11 | 12 | const onClick = () => { 13 | if (onModalClose) setOnModalClose(onModalClose); 14 | setModalContent(modalComponent); 15 | if (onButtonClick) onButtonClick(); 16 | }; 17 | 18 | return ( 19 | 20 | ); 21 | } 22 | 23 | export default OpenModalButton; -------------------------------------------------------------------------------- /react-app/src/components/SignupFormModal/SignupForm.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appacademy/practice-for-week-19-python-project-skeleton/a4d103956d7174d37a4b35db55a6281f197712f5/react-app/src/components/SignupFormModal/SignupForm.css -------------------------------------------------------------------------------- /react-app/src/components/SignupFormModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { useModal } from "../../context/Modal"; 4 | import { signUp } from "../../store/session"; 5 | import "./SignupForm.css"; 6 | 7 | function SignupFormModal() { 8 | const dispatch = useDispatch(); 9 | const [email, setEmail] = useState(""); 10 | const [username, setUsername] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const [confirmPassword, setConfirmPassword] = useState(""); 13 | const [errors, setErrors] = useState([]); 14 | const { closeModal } = useModal(); 15 | 16 | const handleSubmit = async (e) => { 17 | e.preventDefault(); 18 | if (password === confirmPassword) { 19 | const data = await dispatch(signUp(username, email, password)); 20 | if (data) { 21 | setErrors(data); 22 | } else { 23 | closeModal(); 24 | } 25 | } else { 26 | setErrors([ 27 | "Confirm Password field must be the same as the Password field", 28 | ]); 29 | } 30 | }; 31 | 32 | return ( 33 | <> 34 |

Sign Up

35 |
36 |
    37 | {errors.map((error, idx) => ( 38 |
  • {error}
  • 39 | ))} 40 |
41 | 50 | 59 | 68 | 77 | 78 |
79 | 80 | ); 81 | } 82 | 83 | export default SignupFormModal; -------------------------------------------------------------------------------- /react-app/src/components/SignupFormPage/SignupForm.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appacademy/practice-for-week-19-python-project-skeleton/a4d103956d7174d37a4b35db55a6281f197712f5/react-app/src/components/SignupFormPage/SignupForm.css -------------------------------------------------------------------------------- /react-app/src/components/SignupFormPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { Redirect } from "react-router-dom"; 4 | import { signUp } from "../../store/session"; 5 | import './SignupForm.css'; 6 | 7 | function SignupFormPage() { 8 | const dispatch = useDispatch(); 9 | const sessionUser = useSelector((state) => state.session.user); 10 | const [email, setEmail] = useState(""); 11 | const [username, setUsername] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [confirmPassword, setConfirmPassword] = useState(""); 14 | const [errors, setErrors] = useState([]); 15 | 16 | if (sessionUser) return ; 17 | 18 | const handleSubmit = async (e) => { 19 | e.preventDefault(); 20 | if (password === confirmPassword) { 21 | const data = await dispatch(signUp(username, email, password)); 22 | if (data) { 23 | setErrors(data) 24 | } 25 | } else { 26 | setErrors(['Confirm Password field must be the same as the Password field']); 27 | } 28 | }; 29 | 30 | return ( 31 | <> 32 |

Sign Up

33 |
34 |
    35 | {errors.map((error, idx) =>
  • {error}
  • )} 36 |
37 | 46 | 55 | 64 | 73 | 74 |
75 | 76 | ); 77 | } 78 | 79 | export default SignupFormPage; 80 | -------------------------------------------------------------------------------- /react-app/src/components/auth/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | const ProtectedRoute = props => { 6 | const user = useSelector(state => state.session.user) 7 | return ( 8 | 9 | {(user)? props.children : } 10 | 11 | ) 12 | }; 13 | 14 | 15 | export default ProtectedRoute; 16 | -------------------------------------------------------------------------------- /react-app/src/context/Modal.css: -------------------------------------------------------------------------------- 1 | #modal { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | bottom: 0; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | #modal-background { 13 | position: fixed; 14 | top: 0; 15 | right: 0; 16 | left: 0; 17 | bottom: 0; 18 | background-color: rgba(0, 0, 0, 0.7); 19 | } 20 | 21 | #modal-content { 22 | position: absolute; 23 | background-color:white; 24 | } -------------------------------------------------------------------------------- /react-app/src/context/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useContext } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './Modal.css'; 4 | 5 | const ModalContext = React.createContext(); 6 | 7 | export function ModalProvider({ children }) { 8 | const modalRef = useRef(); 9 | const [modalContent, setModalContent] = useState(null); 10 | // callback function that will be called when modal is closing 11 | const [onModalClose, setOnModalClose] = useState(null); 12 | 13 | const closeModal = () => { 14 | setModalContent(null); // clear the modal contents 15 | // If callback function is truthy, call the callback function and reset it 16 | // to null: 17 | if (typeof onModalClose === 'function') { 18 | setOnModalClose(null); 19 | onModalClose(); 20 | } 21 | }; 22 | 23 | const contextValue = { 24 | modalRef, // reference to modal div 25 | modalContent, // React component to render inside modal 26 | setModalContent, // function to set the React component to render inside modal 27 | setOnModalClose, // function to set the callback function called when modal is closing 28 | closeModal // function to close the modal 29 | }; 30 | 31 | return ( 32 | <> 33 | 34 | {children} 35 | 36 |
37 | 38 | ); 39 | } 40 | 41 | export function Modal() { 42 | const { modalRef, modalContent, closeModal } = useContext(ModalContext); 43 | // If there is no div referenced by the modalRef or modalContent is not a 44 | // truthy value, render nothing: 45 | if (!modalRef || !modalRef.current || !modalContent) return null; 46 | 47 | // Render the following component to the div referenced by the modalRef 48 | return ReactDOM.createPortal( 49 |