├── .dockerignore ├── .env.example ├── .flaskenv ├── .gitignore ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── auth_routes.py │ ├── file_routes.py │ └── user_routes.py ├── aws_s3.py ├── config.py ├── forms │ ├── __init__.py │ ├── login_form.py │ └── signup_form.py ├── models │ ├── __init__.py │ ├── db.py │ ├── file.py │ └── user.py └── seeds │ ├── __init__.py │ └── demo_user.py ├── dev-requirements.txt ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 09bf8cf1f17f_create_tables.py │ └── e7016e018da0_create_files_table.py ├── react-app ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── components │ ├── FileUpload.js │ ├── NavBar.js │ ├── UploadFile.js │ ├── User.js │ ├── UsersList.js │ └── auth │ │ ├── LoginForm.js │ │ ├── LogoutButton.js │ │ ├── ProtectedRoute.js │ │ └── SignUpForm.js │ ├── index.css │ ├── index.js │ └── store │ ├── files.js │ ├── index.js │ ├── session.js │ └── users.js └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | react-app/node_modules 2 | .venv 3 | Pipfile 4 | Pipfile.lock 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=postgresql://starter_app_dev:password@localhost/starter_app 5 | S3_BUCKET_NAME= 6 | S3_ACCESS_KEY= 7 | S3_SECRET_ACCESS_KEY= 8 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vim 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/flask 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=flask 5 | 6 | ### Flask ### 7 | instance/* 8 | !instance/.gitignore 9 | .webassets-cache 10 | 11 | ### Flask.Python Stack ### 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | pytestdebug.log 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | doc/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | pythonenv* 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | 147 | # profiling data 148 | .prof 149 | 150 | # End of https://www.toptal.com/developers/gitignore/api/flask 151 | 152 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 AS build-stage 2 | 3 | WORKDIR /react-app 4 | 5 | COPY react-app/. . 6 | 7 | ENV REACT_APP_BASE_URL= 8 | 9 | RUN npm install 10 | RUN npm run build 11 | 12 | FROM python:3.8 13 | 14 | ENV FLASK_APP=app 15 | ENV FLASK_ENV=production 16 | ENV SQLALCHEMY_ECHO=True 17 | 18 | EXPOSE 8000 19 | 20 | WORKDIR /var/www 21 | COPY . . 22 | COPY --from=build-stage /react-app/build/* app/static/ 23 | 24 | RUN pip install -r requirements.txt 25 | RUN pip install psycopg2 26 | 27 | CMD gunicorn app:app 28 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | alembic = "==1.5.5" 8 | python-dotenv = "==0.15.0" 9 | boto3 = "==1.17.16" 10 | click = "==7.1.2" 11 | gunicorn = "==20.0.4" 12 | itsdangerous = "==1.1.0" 13 | email-validator = "==1.1.2" 14 | Flask = "==1.1.2" 15 | Flask-WTF = "==0.14.3" 16 | WTForms = "==2.3.3" 17 | Flask-SQLAlchemy = "==2.4.4" 18 | SQLAlchemy = "==1.3.23" 19 | Flask-Migrate = "==2.7.0" 20 | Flask-Cors = "==3.0.10" 21 | Werkzeug = "==1.0.1" 22 | Flask-JWT-Extended = "==4.0.2" 23 | Flask-Login = "==0.5.0" 24 | PyJWT = "==2.0.1" 25 | botocore = "==1.20.16" 26 | dnspython = "==2.1.0" 27 | idna = "==3.1" 28 | jmespath = "==0.10.0" 29 | python-dateutil = "==2.8.1" 30 | python-editor = "==1.0.4" 31 | s3transfer = "==0.3.4" 32 | six = "==1.15.0" 33 | urllib3 = "==1.26.3" 34 | Jinja2 = "==2.11.3" 35 | Mako = "==1.1.4" 36 | MarkupSafe = "==1.1.1" 37 | 38 | [dev-packages] 39 | psycopg2-binary = "==2.8.6" 40 | pylint = "==2.7.1" 41 | autopep8 = "==1.5.5" 42 | astroid = "==2.5.0" 43 | isort = "==5.7.0" 44 | lazy-object-proxy = "==1.5.2" 45 | mccabe = "==0.6.1" 46 | pycodestyle = "==2.6.0" 47 | toml = "==0.10.2" 48 | wrapt = "==1.12.1" 49 | 50 | [requires] 51 | python_version = "3.8" 52 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "6dfdf7160a39937f2c07e6953af63d04e02fd4963d3faa88b0dc918134dde11c" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 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:df0028c19275a2cff137e39617a39cdcdbd1173733b87b6bfa257b7c0860213b" 22 | ], 23 | "index": "pypi", 24 | "version": "==1.5.5" 25 | }, 26 | "boto3": { 27 | "hashes": [ 28 | "sha256:602eadaef665f49090344e0f87aa6a98dbe1ccdd2f20069a372ed35f2706c63c", 29 | "sha256:7d52ae25baeef79151147c729051a790b7a8b3858a126d59c9ab957815a6d4e7" 30 | ], 31 | "index": "pypi", 32 | "version": "==1.17.16" 33 | }, 34 | "botocore": { 35 | "hashes": [ 36 | "sha256:48350c0524fafcc6f1cf792a80080eeaf282c4ceed016e9296f1ebfda7c34fb3", 37 | "sha256:dd95871cf8a418ab730a219f2bfc301c98f2d9d0a294e43f51715bdd4aedd4cd" 38 | ], 39 | "index": "pypi", 40 | "version": "==1.20.16" 41 | }, 42 | "click": { 43 | "hashes": [ 44 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 45 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 46 | ], 47 | "index": "pypi", 48 | "version": "==7.1.2" 49 | }, 50 | "dnspython": { 51 | "hashes": [ 52 | "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", 53 | "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" 54 | ], 55 | "index": "pypi", 56 | "version": "==2.1.0" 57 | }, 58 | "email-validator": { 59 | "hashes": [ 60 | "sha256:094b1d1c60d790649989d38d34f69e1ef07792366277a2cf88684d03495d018f", 61 | "sha256:1a13bd6050d1db4475f13e444e169b6fe872434922d38968c67cea9568cce2f0" 62 | ], 63 | "index": "pypi", 64 | "version": "==1.1.2" 65 | }, 66 | "flask": { 67 | "hashes": [ 68 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 69 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 70 | ], 71 | "index": "pypi", 72 | "version": "==1.1.2" 73 | }, 74 | "flask-cors": { 75 | "hashes": [ 76 | "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", 77 | "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" 78 | ], 79 | "index": "pypi", 80 | "version": "==3.0.10" 81 | }, 82 | "flask-jwt-extended": { 83 | "hashes": [ 84 | "sha256:d42015246f61cf7baea474de0657542eb2c3d9b4322cbeac062a418297b265ac", 85 | "sha256:e633ca9dcd73aab5d5de5e603e26767cbf0c94be1619bee04ff512ad3ce129d6" 86 | ], 87 | "index": "pypi", 88 | "version": "==4.0.2" 89 | }, 90 | "flask-login": { 91 | "hashes": [ 92 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", 93 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" 94 | ], 95 | "index": "pypi", 96 | "version": "==0.5.0" 97 | }, 98 | "flask-migrate": { 99 | "hashes": [ 100 | "sha256:26871836a4e46d2d590cf8e558c6d60039e1c003079b240689d845726b6b57c0", 101 | "sha256:ae2f05671588762dd83a21d8b18c51fe355e86783e24594995ff8d7380dffe38" 102 | ], 103 | "index": "pypi", 104 | "version": "==2.7.0" 105 | }, 106 | "flask-sqlalchemy": { 107 | "hashes": [ 108 | "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", 109 | "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" 110 | ], 111 | "index": "pypi", 112 | "version": "==2.4.4" 113 | }, 114 | "flask-wtf": { 115 | "hashes": [ 116 | "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2", 117 | "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720" 118 | ], 119 | "index": "pypi", 120 | "version": "==0.14.3" 121 | }, 122 | "gunicorn": { 123 | "hashes": [ 124 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", 125 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" 126 | ], 127 | "index": "pypi", 128 | "version": "==20.0.4" 129 | }, 130 | "idna": { 131 | "hashes": [ 132 | "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", 133 | "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" 134 | ], 135 | "index": "pypi", 136 | "version": "==3.1" 137 | }, 138 | "itsdangerous": { 139 | "hashes": [ 140 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 141 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 142 | ], 143 | "index": "pypi", 144 | "version": "==1.1.0" 145 | }, 146 | "jinja2": { 147 | "hashes": [ 148 | "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", 149 | "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" 150 | ], 151 | "index": "pypi", 152 | "version": "==2.11.3" 153 | }, 154 | "jmespath": { 155 | "hashes": [ 156 | "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", 157 | "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" 158 | ], 159 | "index": "pypi", 160 | "version": "==0.10.0" 161 | }, 162 | "mako": { 163 | "hashes": [ 164 | "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab" 165 | ], 166 | "index": "pypi", 167 | "version": "==1.1.4" 168 | }, 169 | "markupsafe": { 170 | "hashes": [ 171 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 172 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 173 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 174 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 175 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 176 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 177 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 178 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 179 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 180 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 181 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 182 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 183 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 184 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 185 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 186 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 187 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 188 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 189 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 190 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 191 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 192 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 193 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 194 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 195 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 196 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 197 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 198 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 199 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 200 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 201 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 202 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 203 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 204 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 205 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 206 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 207 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 208 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 209 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 210 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 211 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 212 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 213 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 214 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 215 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 216 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 217 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 218 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 219 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 220 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 221 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 222 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 223 | ], 224 | "index": "pypi", 225 | "version": "==1.1.1" 226 | }, 227 | "pyjwt": { 228 | "hashes": [ 229 | "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7", 230 | "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847" 231 | ], 232 | "index": "pypi", 233 | "version": "==2.0.1" 234 | }, 235 | "python-dateutil": { 236 | "hashes": [ 237 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 238 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 239 | ], 240 | "index": "pypi", 241 | "version": "==2.8.1" 242 | }, 243 | "python-dotenv": { 244 | "hashes": [ 245 | "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e", 246 | "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0" 247 | ], 248 | "index": "pypi", 249 | "version": "==0.15.0" 250 | }, 251 | "python-editor": { 252 | "hashes": [ 253 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 254 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 255 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 256 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 257 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 258 | ], 259 | "index": "pypi", 260 | "version": "==1.0.4" 261 | }, 262 | "s3transfer": { 263 | "hashes": [ 264 | "sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed", 265 | "sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2" 266 | ], 267 | "index": "pypi", 268 | "version": "==0.3.4" 269 | }, 270 | "six": { 271 | "hashes": [ 272 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 273 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 274 | ], 275 | "index": "pypi", 276 | "version": "==1.15.0" 277 | }, 278 | "sqlalchemy": { 279 | "hashes": [ 280 | "sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862", 281 | "sha256:1fe5d8d39118c2b018c215c37b73fd6893c3e1d4895be745ca8ff6eb83333ed3", 282 | "sha256:23927c3981d1ec6b4ea71eb99d28424b874d9c696a21e5fbd9fa322718be3708", 283 | "sha256:24f9569e82a009a09ce2d263559acb3466eba2617203170e4a0af91e75b4f075", 284 | "sha256:2578dbdbe4dbb0e5126fb37ffcd9793a25dcad769a95f171a2161030bea850ff", 285 | "sha256:269990b3ab53cb035d662dcde51df0943c1417bdab707dc4a7e4114a710504b4", 286 | "sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1", 287 | "sha256:37b83bf81b4b85dda273aaaed5f35ea20ad80606f672d94d2218afc565fb0173", 288 | "sha256:63677d0c08524af4c5893c18dbe42141de7178001360b3de0b86217502ed3601", 289 | "sha256:639940bbe1108ac667dcffc79925db2966826c270112e9159439ab6bb14f8d80", 290 | "sha256:6a939a868fdaa4b504e8b9d4a61f21aac11e3fecc8a8214455e144939e3d2aea", 291 | "sha256:6b8b8c80c7f384f06825612dd078e4a31f0185e8f1f6b8c19e188ff246334205", 292 | "sha256:6c9e6cc9237de5660bcddea63f332428bb83c8e2015c26777281f7ffbd2efb84", 293 | "sha256:6ec1044908414013ebfe363450c22f14698803ce97fbb47e53284d55c5165848", 294 | "sha256:6fca33672578666f657c131552c4ef8979c1606e494f78cd5199742dfb26918b", 295 | "sha256:751934967f5336a3e26fc5993ccad1e4fee982029f9317eb6153bc0bc3d2d2da", 296 | "sha256:8be835aac18ec85351385e17b8665bd4d63083a7160a017bef3d640e8e65cadb", 297 | "sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5", 298 | "sha256:94208867f34e60f54a33a37f1c117251be91a47e3bfdb9ab8a7847f20886ad06", 299 | "sha256:94f667d86be82dd4cb17d08de0c3622e77ca865320e0b95eae6153faa7b4ecaf", 300 | "sha256:9e9c25522933e569e8b53ccc644dc993cab87e922fb7e142894653880fdd419d", 301 | "sha256:a0e306e9bb76fd93b29ae3a5155298e4c1b504c7cbc620c09c20858d32d16234", 302 | "sha256:a8bfc1e1afe523e94974132d7230b82ca7fa2511aedde1f537ec54db0399541a", 303 | "sha256:ac2244e64485c3778f012951fdc869969a736cd61375fde6096d08850d8be729", 304 | "sha256:b4b0e44d586cd64b65b507fa116a3814a1a53d55dce4836d7c1a6eb2823ff8d1", 305 | "sha256:baeb451ee23e264de3f577fee5283c73d9bbaa8cb921d0305c0bbf700094b65b", 306 | "sha256:c7dc052432cd5d060d7437e217dd33c97025287f99a69a50e2dc1478dd610d64", 307 | "sha256:d1a85dfc5dee741bf49cb9b6b6b8d2725a268e4992507cf151cba26b17d97c37", 308 | "sha256:d90010304abb4102123d10cbad2cdf2c25a9f2e66a50974199b24b468509bad5", 309 | "sha256:ddfb511e76d016c3a160910642d57f4587dc542ce5ee823b0d415134790eeeb9", 310 | "sha256:e273367f4076bd7b9a8dc2e771978ef2bfd6b82526e80775a7db52bff8ca01dd", 311 | "sha256:e5bb3463df697279e5459a7316ad5a60b04b0107f9392e88674d0ece70e9cf70", 312 | "sha256:e8a1750b44ad6422ace82bf3466638f1aa0862dbb9689690d5f2f48cce3476c8", 313 | "sha256:eab063a70cca4a587c28824e18be41d8ecc4457f8f15b2933584c6c6cccd30f0", 314 | "sha256:ecce8c021894a77d89808222b1ff9687ad84db54d18e4bd0500ca766737faaf6", 315 | "sha256:f4d972139d5000105fcda9539a76452039434013570d6059993120dc2a65e447", 316 | "sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec", 317 | "sha256:fdd2ed7395df8ac2dbb10cefc44737b66c6a5cd7755c92524733d7a443e5b7e2" 318 | ], 319 | "index": "pypi", 320 | "version": "==1.3.23" 321 | }, 322 | "urllib3": { 323 | "hashes": [ 324 | "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", 325 | "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" 326 | ], 327 | "index": "pypi", 328 | "version": "==1.26.3" 329 | }, 330 | "werkzeug": { 331 | "hashes": [ 332 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 333 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 334 | ], 335 | "index": "pypi", 336 | "version": "==1.0.1" 337 | }, 338 | "wtforms": { 339 | "hashes": [ 340 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c", 341 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c" 342 | ], 343 | "index": "pypi", 344 | "version": "==2.3.3" 345 | } 346 | }, 347 | "develop": { 348 | "astroid": { 349 | "hashes": [ 350 | "sha256:87ae7f2398b8a0ae5638ddecf9987f081b756e0e9fc071aeebdca525671fc4dc", 351 | "sha256:b31c92f545517dcc452f284bc9c044050862fbe6d93d2b3de4a215a6b384bf0d" 352 | ], 353 | "index": "pypi", 354 | "version": "==2.5.0" 355 | }, 356 | "autopep8": { 357 | "hashes": [ 358 | "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea", 359 | "sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443" 360 | ], 361 | "index": "pypi", 362 | "version": "==1.5.5" 363 | }, 364 | "isort": { 365 | "hashes": [ 366 | "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", 367 | "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" 368 | ], 369 | "index": "pypi", 370 | "version": "==5.7.0" 371 | }, 372 | "lazy-object-proxy": { 373 | "hashes": [ 374 | "sha256:1d33d6f789697f401b75ce08e73b1de567b947740f768376631079290118ad39", 375 | "sha256:2f2de8f8ac0be3e40d17730e0600619d35c78c13a099ea91ef7fb4ad944ce694", 376 | "sha256:3782931963dc89e0e9a0ae4348b44762e868ea280e4f8c233b537852a8996ab9", 377 | "sha256:37d9c34b96cca6787fe014aeb651217944a967a5b165e2cacb6b858d2997ab84", 378 | "sha256:38c3865bd220bd983fcaa9aa11462619e84a71233bafd9c880f7b1cb753ca7fa", 379 | "sha256:429c4d1862f3fc37cd56304d880f2eae5bd0da83bdef889f3bd66458aac49128", 380 | "sha256:522b7c94b524389f4a4094c4bf04c2b02228454ddd17c1a9b2801fac1d754871", 381 | "sha256:57fb5c5504ddd45ed420b5b6461a78f58cbb0c1b0cbd9cd5a43ad30a4a3ee4d0", 382 | "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4", 383 | "sha256:6f4e5e68b7af950ed7fdb594b3f19a0014a3ace0fedb86acb896e140ffb24302", 384 | "sha256:71a1ef23f22fa8437974b2d60fedb947c99a957ad625f83f43fd3de70f77f458", 385 | "sha256:8a44e9901c0555f95ac401377032f6e6af66d8fc1fbfad77a7a8b1a826e0b93c", 386 | "sha256:b6577f15d5516d7d209c1a8cde23062c0f10625f19e8dc9fb59268859778d7d7", 387 | "sha256:c8fe2d6ff0ff583784039d0255ea7da076efd08507f2be6f68583b0da32e3afb", 388 | "sha256:cadfa2c2cf54d35d13dc8d231253b7985b97d629ab9ca6e7d672c35539d38163", 389 | "sha256:cd1bdace1a8762534e9a36c073cd54e97d517a17d69a17985961265be6d22847", 390 | "sha256:ddbdcd10eb999d7ab292677f588b658372aadb9a52790f82484a37127a390108", 391 | "sha256:e7273c64bccfd9310e9601b8f4511d84730239516bada26a0c9846c9697617ef", 392 | "sha256:e7428977763150b4cf83255625a80a23dfdc94d43be7791ce90799d446b4e26f", 393 | "sha256:e960e8be509e8d6d618300a6c189555c24efde63e85acaf0b14b2cd1ac743315", 394 | "sha256:ecb5dd5990cec6e7f5c9c1124a37cb2c710c6d69b0c1a5c4aa4b35eba0ada068", 395 | "sha256:ef3f5e288aa57b73b034ce9c1f1ac753d968f9069cd0742d1d69c698a0167166", 396 | "sha256:fa5b2dee0e231fa4ad117be114251bdfe6afe39213bd629d43deb117b6a6c40a", 397 | "sha256:fa7fb7973c622b9e725bee1db569d2c2ee64d2f9a089201c5e8185d482c7352d" 398 | ], 399 | "index": "pypi", 400 | "version": "==1.5.2" 401 | }, 402 | "mccabe": { 403 | "hashes": [ 404 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 405 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 406 | ], 407 | "index": "pypi", 408 | "version": "==0.6.1" 409 | }, 410 | "psycopg2-binary": { 411 | "hashes": [ 412 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 413 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 414 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 415 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", 416 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 417 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 418 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 419 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", 420 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 421 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 422 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 423 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 424 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", 425 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 426 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 427 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 428 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 429 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 430 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 431 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 432 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 433 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 434 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 435 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 436 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 437 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 438 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 439 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 440 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 441 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 442 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 443 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 444 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 445 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 446 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 447 | ], 448 | "index": "pypi", 449 | "version": "==2.8.6" 450 | }, 451 | "pycodestyle": { 452 | "hashes": [ 453 | "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", 454 | "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" 455 | ], 456 | "index": "pypi", 457 | "version": "==2.6.0" 458 | }, 459 | "pylint": { 460 | "hashes": [ 461 | "sha256:81ce108f6342421169ea039ff1f528208c99d2e5a9c4ca95cfc5291be6dfd982", 462 | "sha256:a251b238db462b71d25948f940568bb5b3ae0e37dbaa05e10523f54f83e6cc7e" 463 | ], 464 | "index": "pypi", 465 | "version": "==2.7.1" 466 | }, 467 | "toml": { 468 | "hashes": [ 469 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 470 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 471 | ], 472 | "index": "pypi", 473 | "version": "==0.10.2" 474 | }, 475 | "wrapt": { 476 | "hashes": [ 477 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 478 | ], 479 | "index": "pypi", 480 | "version": "==1.12.1" 481 | } 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS S3 Demo with Flask-React Stack 2 | 3 | - PostgreSQL 4 | - Flask 5 | - React 6 | 7 | ## Set Up 8 | 9 | **Packages to install in your backend** 10 | 11 | - [boto3](https://github.com/boto/boto3) 12 | 13 | ```bash 14 | pipenv install boto3 15 | ``` 16 | 17 | Don't forget to run `pipenv lock -r > requirements.txt` after installing boto3! 18 | 19 | **Create your AWS User and Bucket** 20 | 21 | 1. Navigate to [aws.amazon.com] and `Create an AWS Account`. 22 | 2. Once signed into the AWS console, search for `S3: Scalable Storage in the 23 | Cloud` and click the link to go to the S3 Management Console 24 | 3. Once there, click `Create Bucket` 25 | 4. In the Create Bucket form, enter a name, choose a region, and leave all other 26 | options as default. 27 | - You can opt to make all of your content in this bucket public by 28 | unchecking the checkbox by `Block all public access` AND checking the 29 | acknowledgment checkbox that shows up below. 30 | - Create the bucket 31 | - You're going to need the name of your bucket in a second, so you'll want 32 | to keep this tab with your bucket open. 33 | 5. In the top search bar, search for `IAM: Manage access to AWS resources` and 34 | open the link in a new tab. This should take you to the Identity and Access 35 | Management (IAM) Console. 36 | 6. Once there, click `Users` under `Access management` in the left sidebar. 37 | 7. Click `Add user`. 38 | 8. Name the user whatever you like and give the user `Programmatic access`. 39 | Click `Next: Permissions` 40 | 9. Here is where you'll set up the security policy for your new user. 41 | - Click `Attach existing policies directly` 42 | - Click `Create Policy`. This will open up in a new tab. 43 | 10. In the new tab, click on the `JSON` tab and paste the following: 44 | 45 | ```json 46 | { 47 | "Version": "2012-10-17", 48 | "Statement": [ 49 | { 50 | "Sid": "Stmt1420751757000", 51 | "Effect": "Allow", 52 | "Action": ["s3:*"], 53 | "Resource": "arn:aws:s3:::/*" 54 | } 55 | ] 56 | } 57 | ``` 58 | 59 | - Make sure to replace `` with the name of your bucket. 60 | - Click `Next: Tags` 61 | - Click `Next: Review` 62 | - Give the policy a name (maybe something like `s3-access-to-name-of-project`). 63 | - Click `Create policy` 64 | 65 | 10. After creating the policy, go back to the IAM Management Console where you 66 | were creating a user. 67 | 12. Click the refresh icon in the middle of the screen above the table of 68 | policies. 69 | 13. Search for your new policy and check the checkbox to the left of it. 70 | 14. Click `Next: Tags` 71 | 15. Click `Next: Review` 72 | 16. Review your new user and make sure the policy you've attached is correct. 73 | Then click `Create user`. 74 | 17. You will now get the `Access Key ID` and the `Secret Access Key`. Make sure 75 | to save both somewhere safe. You can (should) download the `.csv` file. 76 | **Store this somewhere safe on your computer.** 77 | - Note: If you don't somehow get your keys here, you will have to generate 78 | new keys through IAM because AWS will not give you an old secret key after 79 | this page. 80 | 81 | [aws.amazon.com]: https://aws.amazon.com/ 82 | 83 | ## Set up AWS S3 in your backend 84 | 85 | #### `.env` 86 | 87 | Now that you have your AWS keys, you will need to set them in your `.env` file. 88 | 89 | ```env 90 | 91 | S3_BUCKET_NAME= 92 | S3_ACCESS_KEY= 93 | S3_SECRET_ACCESS_KEY= 94 | ``` 95 | 96 | Make sure that these are set in your BACKEND `.env` file (the one in the root of 97 | your project). Now is a very good time to double-check that your `.env` is 98 | listed in your backend `.gitignore`. 99 | 100 | _Note: You will need to provide these keys to Heroku when you are ready to 101 | deploy._ 102 | 103 | #### `config.py` 104 | 105 | Now that we've added our AWS Keys to our `.env`, we will want to access them 106 | through the rest of our app. To do that, add the following lines to your Config 107 | class inside your `config.py` file. 108 | 109 | ```python 110 | S3_BUCKET = os.environ.get("S3_BUCKET_NAME") 111 | S3_KEY = os.environ.get("S3_ACCESS_KEY") 112 | S3_SECRET = os.environ.get("S3_SECRET_ACCESS_KEY") 113 | S3_LOCATION = f"http://{S3_BUCKET}.s3.amazonaws.com/" 114 | ``` 115 | 116 | Your entire `config.py` file should now look something like this: 117 | 118 | ```python 119 | import os 120 | 121 | 122 | class Config: 123 | SECRET_KEY = os.environ.get('SECRET_KEY') 124 | SQLALCHEMY_TRACK_MODIFICATIONS = False 125 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 126 | SQLALCHEMY_ECHO = True 127 | 128 | S3_BUCKET = os.environ.get("S3_BUCKET_NAME") 129 | S3_KEY = os.environ.get("S3_ACCESS_KEY") 130 | S3_SECRET = os.environ.get("S3_SECRET_ACCESS_KEY") 131 | S3_LOCATION = f"http://{S3_BUCKET}.s3.amazonaws.com/" 132 | ``` 133 | 134 | #### `aws_s3.py` 135 | 136 | Make a file called `aws_s3.py` as a module inside of your Flask `app` directory. 137 | Copy the following code inside: 138 | 139 | ```python 140 | import boto3 141 | import botocore 142 | from .config import Config 143 | 144 | 145 | s3 = boto3.client( 146 | "s3", 147 | aws_access_key_id=Config.S3_KEY, 148 | aws_secret_access_key=Config.S3_SECRET 149 | ) 150 | ``` 151 | 152 | In here, you will see that you are using the `boto3.client` method to connect to 153 | your AWS S3 bucket. This works because we are able to pass it your AWS Keys that 154 | we are grabbing from your Config object, `Config.S3_KEY` and `Config.S3_SECRET`. 155 | 156 | Now copy this into your file: 157 | 158 | ```python 159 | 160 | def upload_file_to_s3(file, bucket_name, acl="public-read"): 161 | 162 | try: 163 | 164 | s3.upload_fileobj( 165 | file, 166 | bucket_name, 167 | file.filename, 168 | ExtraArgs={ 169 | "ACL": acl, 170 | "ContentType": file.content_type 171 | } 172 | ) 173 | 174 | except Exception as e: 175 | # This is a catch all exception, edit this part to fit your needs. 176 | print("Something Happened: ", e) 177 | return e 178 | ``` 179 | 180 | Here we are defining our function that will allow you to store a file to your S3 181 | bucket. Notice that it takes in a `file`, `bucket_name` and an argument called 182 | `acl` that is set to `"public-read"` by default. Because of this default param, 183 | when we are ready to call this function we only need to pass in our `file` and 184 | our `bucket_name`. 185 | 186 | Also note that inside our function we are calling the `s3.upload_fileobj` 187 | method. In addition to passing this method our `file` and `bucket_name`, we are 188 | giving it an `ExrtaArgs` object that contains our POST request headers. Thanks 189 | to these `ExtraArgs` we do not need to specify any request headers when making 190 | our POST request. 191 | 192 | Lastly, copy the following return statement to the end of your function. This 193 | return statement will give us the URL to the file we've just uploaded to our 194 | bucket. 195 | 196 | ```python 197 | return f"{Config.S3_LOCATION}{file.filename}" 198 | ``` 199 | 200 | By now, your whole `aws_s3.py` file should look like this: 201 | 202 | ```python 203 | 204 | import boto3 205 | import botocore 206 | from .config import Config 207 | 208 | 209 | s3 = boto3.client( 210 | "s3", 211 | aws_access_key_id=Config.S3_KEY, 212 | aws_secret_access_key=Config.S3_SECRET 213 | ) 214 | 215 | def upload_file_to_s3(file, bucket_name, acl="public-read"): 216 | 217 | try: 218 | 219 | s3.upload_fileobj( 220 | file, 221 | bucket_name, 222 | file.filename, 223 | ExtraArgs={ 224 | "ACL": acl, 225 | "ContentType": file.content_type 226 | } 227 | ) 228 | 229 | except Exception as e: 230 | # This is a catch all exception, edit this part to fit your needs. 231 | print("Something Happened: ", e) 232 | return e 233 | 234 | 235 | return f"{Config.S3_LOCATION}{file.filename}" 236 | ``` 237 | 238 | #### If you haven't already: 239 | 240 | ### MAKE SURE TO GITIGNORE YOUR .ENV FILE 241 | 242 | ## Sending Your POST request 243 | 244 | Now it's time to set up our POST request by way of a Redux thunk. Hopefully you 245 | know by how to submit a form from your React component through to a thunk! The 246 | form we are receiving should contain the file we are intending to upload along 247 | with all other necessary fields to persist to our database. An example looks 248 | like this: 249 | 250 | ```javascript 251 | export const uploadFile = (fileForm) => async (dispatch) => { 252 | const { 253 | user_id, 254 | /* all, 255 | other, 256 | form, 257 | fields, */ 258 | file, // this is the file for uploading 259 | } = fileForm; 260 | }; 261 | ``` 262 | 263 | Here we destructure the file and associated form fields from the initial form 264 | submission. Next we will package them up in a new upload-worthy style form. 265 | 266 | ```javascript 267 | const form = new FormData(); 268 | form.append("user_id", user_id); 269 | // repeat as necessary for each required form field 270 | form.append("file", file); 271 | ``` 272 | 273 | We've now created a new FormData object and appended our file and associated 274 | form fields. This object is now ready to submit to our backend to get persisted 275 | in our data base. We will do so by setting the following: 276 | 277 | ```javascript 278 | const res = await fetch("/api/", { 279 | method: "POST", 280 | body: form, 281 | }); 282 | ``` 283 | 284 | Remember how we already prescribed our request headed back in our `aws_s3.py`? 285 | No need to set any here! 286 | 287 | Your overall thunk should be looking like this (with `` replaced 288 | by your actual API route): 289 | 290 | ```javascript 291 | export const uploadFile = (fileForm) => async (dispatch) => { 292 | const { 293 | user_id, 294 | /* all, 295 | other, 296 | form, 297 | fields, */ 298 | file, // this is the file for uploading 299 | } = fileForm; 300 | 301 | const form = new FormData(); 302 | form.append("user_id", user_id); 303 | // repeat as necessary for each required form field 304 | form.append("file", file); 305 | 306 | const res = await fetch("/api/", { 307 | method: "POST", 308 | body: form, 309 | }); 310 | }; 311 | ``` 312 | 313 | We'll let you figure out what to do with the rest of the thunk, but for now 314 | she's ready to hit your backend! 315 | 316 | ## Setting Your Route 317 | 318 | Here we will set up our route that will call our `upload_file_to_s3` function 319 | that we've defined in our `aws_s3.py` file. We will then push the resulting URL 320 | to our database. 321 | 322 | If you haven't already, create a `route` file inside the `api` directory of your 323 | app. The path should look something like this: `app/api/` 324 | 325 | Begin by including these import statements along with all of your usual ones. 326 | 327 | ```python 328 | import boto3 329 | import botocore 330 | from flask import Blueprint, request 331 | from flask_login import login_required 332 | 333 | from app.config import Config 334 | from app.aws_s3 import * 335 | from app.models import db, 336 | #any other imports as needed 337 | ``` 338 | 339 | Be sure to replace `` with the name of the Model you will be 340 | persisting to. 341 | 342 | Next we will define our route. For the sake of this walkthrough let's call it 343 | `file_routes`. This where we will call our file uploading function as well 344 | receive the file we will be passing into it. This is where the magic happens! 345 | 346 | ```python 347 | 348 | file_routes = Blueprint('file', __name__) 349 | 350 | #Don't forget to register your Blueprint 351 | 352 | @file_routes.route('/', methods=["POST"]) 353 | @login_required 354 | def upload_file: 355 | if "file" not in request.files: 356 | return "No user_file key in request.files" 357 | 358 | file = request.files["file"] 359 | 360 | if file: 361 | file_url = upload_file_to_s3(file, Config.S3_BUCKET) 362 | # create an instance of 363 | file = File( 364 | user_id=request.form.get('user_id') 365 | # extract any form fields you've appended to the 366 | # body of your POST request 367 | # i.e. 368 | url=file_url 369 | ) 370 | db.session.add(file) 371 | db.session.commit() 372 | return file.to_dict() 373 | else: return No File Attached! 374 | ``` 375 | 376 | Here we've used Flask's request object to parse our POST request, which allows 377 | us to pass our intended file through the `upload_file_to_s3` method and retrieve 378 | our AWS URL. We can further use `request.form.get('')` to parse out 379 | each incoming form field and add them to our Model instance. Finally we pass in 380 | our `file_url` that we received from our S3 Bucket and assign it to the 381 | specified column in our model. 382 | 383 | Your whole `route.py` file should look something like this: 384 | 385 | ```python 386 | from flask import Blueprint, request 387 | from flask_login import login_required 388 | 389 | from app.config import Config 390 | from app.aws_s3 import upload_file_to_s3 391 | from app.models import db, 392 | #any other imports as needed 393 | 394 | file_routes = Blueprint('file', __name__) 395 | 396 | #Don't forget to register your Blueprint 397 | 398 | @file_routes.route('/', methods=["POST"]) 399 | @login_required 400 | def upload_file: 401 | if "file" not in request.files: 402 | return "No user_file key in request.files" 403 | 404 | file = request.files["file"] 405 | 406 | if file: 407 | file_url = upload_file_to_s3(file, Config.S3_BUCKET) 408 | # create an instance of 409 | file = File( 410 | user_id=request.form.get('user_id') 411 | #extract any form fields you've appended to the 412 | #body of your POST request 413 | #i.e. 414 | url=file_url 415 | ) 416 | db.session.add(file) 417 | db.session.commit() 418 | return file.to_dict() 419 | else: return No File Attached! 420 | 421 | ``` 422 | 423 | And that's It! If you did everything correctly you should be able to start 424 | storing files to your S3 Bucket and pushing their corresponding URL into your 425 | database. 426 | 427 | ## Public File Uploads 428 | 429 | If you absolutely don't want your files to be publicly available to just any 430 | user, then you want your files to be private. If you don't care if users are 431 | able to access those files, then you can set up the files to be publicly 432 | readable. 433 | 434 | Public upload is recommended for most of the use cases for your portfolio 435 | projects. 436 | 437 | ### Public Files 438 | 439 | How to set up uploading and reading public files on the backend. 440 | 441 | #### Public File Write Configuration 442 | -------------------------------------------------------------------------------- /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 | 8 | from .api import auth_routes, user_routes, file_routes 9 | from .config import Config 10 | from .models import db, User 11 | from .seeds import seed_commands 12 | 13 | app = Flask(__name__) 14 | 15 | login = LoginManager(app) 16 | login.login_view = 'auth.unauthorized' 17 | 18 | 19 | @login.user_loader 20 | def load_user(id): 21 | return User.query.get(int(id)) 22 | 23 | 24 | app.cli.add_command(seed_commands) 25 | 26 | app.config.from_object(Config) 27 | app.register_blueprint(auth_routes, url_prefix='/api/auth') 28 | app.register_blueprint(user_routes, url_prefix='/api/users') 29 | app.register_blueprint(file_routes, url_prefix='/api/files') 30 | db.init_app(app) 31 | Migrate(app, db) 32 | 33 | CORS(app) 34 | 35 | 36 | @app.before_request 37 | def https_redirect(): 38 | if os.environ.get('FLASK_ENV') == 'production': 39 | if request.headers.get('X-Forwarded-Proto') == 'http': 40 | url = request.url.replace('http://', 'https://', 1) 41 | code = 301 42 | return redirect(url, code=code) 43 | 44 | 45 | @app.after_request 46 | def inject_csrf_token(response): 47 | response.set_cookie( 48 | 'csrf_token', 49 | generate_csrf(), 50 | secure=True if os.environ.get('FLASK_ENV') == 'production' else 51 | False, 52 | samesite='Strict' 53 | if os.environ.get('FLASK_ENV') == 'production' else None, 54 | httponly=True 55 | ) 56 | return response 57 | 58 | 59 | @app.route('/', defaults={'path': ''}) 60 | @app.route('/') 61 | def react_root(path): 62 | if path == 'favicon.ico': 63 | return app.send_static_file('favicon.ico') 64 | return app.send_static_file('index.html') 65 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth_routes import auth_routes 2 | from .user_routes import user_routes 3 | from .file_routes import file_routes 4 | -------------------------------------------------------------------------------- /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']}, 401 29 | 30 | 31 | @auth_routes.route('/login', methods=['POST']) 32 | def login(): 33 | """ 34 | Logs a user in 35 | """ 36 | form = LoginForm() 37 | print(request.get_json()) 38 | # Get the csrf_token from the request cookie and put it into the 39 | # form manually to validate_on_submit can be used 40 | form['csrf_token'].data = request.cookies['csrf_token'] 41 | if form.validate_on_submit(): 42 | # Add the user to the session, we are logged in! 43 | user = User.query.filter(User.email == form.data['email']).first() 44 | login_user(user) 45 | return user.to_dict() 46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 47 | 48 | 49 | @auth_routes.route('/logout') 50 | def logout(): 51 | """ 52 | Logs a user out 53 | """ 54 | logout_user() 55 | return {'message': 'User logged out'} 56 | 57 | 58 | @auth_routes.route('/signup', methods=['POST']) 59 | def sign_up(): 60 | """ 61 | Creates a new user and logs them in 62 | """ 63 | form = SignUpForm() 64 | form['csrf_token'].data = request.cookies['csrf_token'] 65 | if form.validate_on_submit(): 66 | user = User( 67 | username=form.data['username'], 68 | email=form.data['email'], 69 | password=form.data['password'] 70 | ) 71 | db.session.add(user) 72 | db.session.commit() 73 | login_user(user) 74 | return user.to_dict() 75 | return {'errors': validation_errors_to_error_messages(form.errors)} 76 | 77 | 78 | @auth_routes.route('/unauthorized') 79 | def unauthorized(): 80 | """ 81 | Returns unauthorized JSON when flask-login authentication fails 82 | """ 83 | return {'errors': ['Unauthorized']}, 401 84 | -------------------------------------------------------------------------------- /app/api/file_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request 2 | from flask_login import login_required 3 | 4 | from app.config import Config 5 | from app.aws_s3 import upload_file_to_s3 6 | from app.models import db, File 7 | 8 | file_routes = Blueprint('file', __name__) 9 | 10 | 11 | @file_routes.route('', methods=["POST"]) 12 | @login_required 13 | def upload_file(): 14 | if "file" not in request.files: 15 | return "No file key in request.files" 16 | 17 | file = request.files["file"] 18 | 19 | if file: 20 | file_url = upload_file_to_s3(file, Config.S3_BUCKET) 21 | file = File( 22 | user_id=request.form.get('user_id'), 23 | url=file_url, 24 | ) 25 | db.session.add(file) 26 | db.session.commit() 27 | return file.to_dict() 28 | return "No File Attached!" 29 | -------------------------------------------------------------------------------- /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 | users = User.query.all() 12 | return {"users": [user.to_dict() for user in users]} 13 | 14 | 15 | @user_routes.route('/') 16 | @login_required 17 | def user(id): 18 | user = User.query.get(id) 19 | return user.to_dict() 20 | -------------------------------------------------------------------------------- /app/aws_s3.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import botocore 3 | from .config import Config 4 | 5 | # These are the allowed file types, edit this part to fit your needs 6 | ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'svg'} 7 | 8 | s3 = boto3.client( 9 | "s3", 10 | aws_access_key_id=Config.S3_KEY, 11 | aws_secret_access_key=Config.S3_SECRET 12 | ) 13 | 14 | 15 | def upload_file_to_s3(file, bucket_name, acl="public-read"): 16 | try: 17 | s3.upload_fileobj(file, 18 | bucket_name, 19 | file.filename, 20 | ExtraArgs={ 21 | "ACL": acl, 22 | "ContentType": file.content_type 23 | }) 24 | 25 | except Exception as e: 26 | # This is catch all exception, edit this part to fit your needs. 27 | print("Something Happened: ", e) 28 | return e 29 | 30 | return f"{Config.S3_LOCATION}{file.filename}" 31 | 32 | 33 | def allowed_file(filename): 34 | return '.' in filename and \ 35 | filename.split('.', 1)[1].lower() in ALLOWED_EXTENSIONS 36 | -------------------------------------------------------------------------------- /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_DATABASE_URI = os.environ.get('DATABASE_URL') 8 | SQLALCHEMY_ECHO = True 9 | 10 | S3_BUCKET = os.environ.get("S3_BUCKET_NAME") 11 | S3_KEY = os.environ.get("S3_ACCESS_KEY") 12 | S3_SECRET = os.environ.get("S3_SECRET_ACCESS_KEY") 13 | S3_LOCATION = f"http://{S3_BUCKET}.s3.amazonaws.com/" 14 | -------------------------------------------------------------------------------- /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 | print("Checking if user exists", field.data) 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 | print("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=[ 29 | DataRequired(), password_matches]) 30 | -------------------------------------------------------------------------------- /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 | print("Checking if user exits", field.data) 9 | email = field.data 10 | user = User.query.filter(User.email == email).first() 11 | if user: 12 | raise ValidationError("User is already registered.") 13 | 14 | 15 | class SignUpForm(FlaskForm): 16 | username = StringField('username', validators=[DataRequired()]) 17 | email = StringField('email', validators=[DataRequired(), user_exists]) 18 | password = StringField('password', validators=[DataRequired()]) 19 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | from .file import File 4 | -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /app/models/file.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | 4 | class File(db.Model): 5 | __tablename__ = 'files' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | user_id = db.Column(db.Integer, db.ForeignKey('users.id')) 9 | url = db.Column(db.String, nullable=False) 10 | 11 | user = db.relationship("User", back_populates="files") 12 | 13 | def to_dict(self): 14 | return { 15 | "id": self.id, 16 | "user": self.user.to_simple_dict(), 17 | "url": self.url, 18 | } 19 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from .db import db 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 | id = db.Column(db.Integer, primary_key=True) 10 | username = db.Column(db.String(40), nullable=False, unique=True) 11 | email = db.Column(db.String(255), nullable=False, unique=True) 12 | hashed_password = db.Column(db.String(255), nullable=False) 13 | 14 | files = db.relationship("File", back_populates="user") 15 | 16 | @property 17 | def password(self): 18 | return self.hashed_password 19 | 20 | @password.setter 21 | def password(self, password): 22 | self.hashed_password = generate_password_hash(password) 23 | 24 | def check_password(self, password): 25 | return check_password_hash(self.password, password) 26 | 27 | def to_dict(self): 28 | return {"id": self.id, "username": self.username, "email": self.email, 29 | "files": [file.to_dict() for file in self.files]} 30 | 31 | def to_simple_dict(self): 32 | return {"id": self.id, "username": self.username, "email": self.email} 33 | -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .demo_user import seed_users, undo_users 3 | 4 | # Creates a seed group to hold our commands 5 | # So we can type `flask seed --help` 6 | seed_commands = AppGroup('seed') 7 | 8 | # Creates the `flask seed all` command 9 | @seed_commands.command('all') 10 | def seed(): 11 | seed_users() 12 | # Add other seed functions here 13 | 14 | # Creates the `flask seed undo` command 15 | @seed_commands.command('undo') 16 | def undo(): 17 | undo_users() 18 | # Add other undo functions here 19 | -------------------------------------------------------------------------------- /app/seeds/demo_user.py: -------------------------------------------------------------------------------- 1 | from werkzeug.security import generate_password_hash 2 | from app.models import db, User 3 | 4 | # Adds a demo user, you can add other users here if you want 5 | def seed_users(): 6 | 7 | demo = User(username='Demo', email='demo@aa.io', 8 | password='password') 9 | 10 | db.session.add(demo) 11 | 12 | db.session.commit() 13 | 14 | # Uses a raw SQL query to TRUNCATE the users table. 15 | # SQLAlchemy doesn't have a built in function to do this 16 | # TRUNCATE Removes all the data from the table, and resets 17 | # the auto incrementing primary key 18 | def undo_users(): 19 | db.session.execute('TRUNCATE users;') 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements --dev-only 6 | # 7 | 8 | -i https://pypi.org/simple 9 | astroid==2.5.0; python_version >= '3.6' 10 | autopep8==1.5.5 11 | isort==5.7.0; python_version >= '3.6' and python_version < '4.0' 12 | lazy-object-proxy==1.5.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 13 | mccabe==0.6.1 14 | psycopg2-binary==2.8.6 15 | pycodestyle==2.6.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 16 | pylint==2.7.1 17 | toml==0.10.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' 18 | wrapt==1.12.1 19 | -------------------------------------------------------------------------------- /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 = %%(rev)s_%%(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,flask_migrate 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 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from flask import current_app 7 | 8 | from alembic import context 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger('alembic.env') 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | config.set_main_option( 24 | 'sqlalchemy.url', 25 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) 26 | target_metadata = current_app.extensions['migrate'].db.metadata 27 | 28 | # other values from the config, defined by the needs of env.py, 29 | # can be acquired: 30 | # my_important_option = config.get_main_option("my_important_option") 31 | # ... etc. 32 | 33 | 34 | def run_migrations_offline(): 35 | """Run migrations in 'offline' mode. 36 | 37 | This configures the context with just a URL 38 | and not an Engine, though an Engine is acceptable 39 | here as well. By skipping the Engine creation 40 | we don't even need a DBAPI to be available. 41 | 42 | Calls to context.execute() here emit the given string to the 43 | script output. 44 | 45 | """ 46 | url = config.get_main_option("sqlalchemy.url") 47 | context.configure( 48 | url=url, target_metadata=target_metadata, literal_binds=True 49 | ) 50 | 51 | with context.begin_transaction(): 52 | context.run_migrations() 53 | 54 | 55 | def run_migrations_online(): 56 | """Run migrations in 'online' mode. 57 | 58 | In this scenario we need to create an Engine 59 | and associate a connection with the context. 60 | 61 | """ 62 | 63 | # this callback is used to prevent an auto-migration from being generated 64 | # when there are no changes to the schema 65 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 66 | def process_revision_directives(context, revision, directives): 67 | if getattr(config.cmd_opts, 'autogenerate', False): 68 | script = directives[0] 69 | if script.upgrade_ops.is_empty(): 70 | directives[:] = [] 71 | logger.info('No changes in schema detected.') 72 | 73 | connectable = current_app.extensions['migrate'].db.engine 74 | 75 | with connectable.connect() as connection: 76 | context.configure( 77 | connection=connection, 78 | target_metadata=target_metadata, 79 | process_revision_directives=process_revision_directives, 80 | **current_app.extensions['migrate'].configure_args 81 | ) 82 | 83 | with context.begin_transaction(): 84 | context.run_migrations() 85 | 86 | 87 | if context.is_offline_mode(): 88 | run_migrations_offline() 89 | else: 90 | run_migrations_online() 91 | -------------------------------------------------------------------------------- /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"} 25 | -------------------------------------------------------------------------------- /migrations/versions/09bf8cf1f17f_create_tables.py: -------------------------------------------------------------------------------- 1 | """create tables 2 | 3 | Revision ID: 09bf8cf1f17f 4 | Revises: 5 | Create Date: 2021-04-09 19:56:21.806498 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '09bf8cf1f17f' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('users', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('username', sa.String(length=40), nullable=False), 24 | sa.Column('email', sa.String(length=255), nullable=False), 25 | sa.Column('hashed_password', sa.String(length=255), nullable=False), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email'), 28 | sa.UniqueConstraint('username') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('users') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/e7016e018da0_create_files_table.py: -------------------------------------------------------------------------------- 1 | """create files table 2 | 3 | Revision ID: e7016e018da0 4 | Revises: 09bf8cf1f17f 5 | Create Date: 2021-04-09 20:08:18.467419 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e7016e018da0' 14 | down_revision = '09bf8cf1f17f' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('files', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('user_id', sa.Integer(), nullable=True), 24 | sa.Column('url', sa.String(), nullable=False), 25 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('files') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /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* 24 | -------------------------------------------------------------------------------- /react-app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /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.11.9", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.7.3", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-redux": "^7.2.2", 12 | "react-router-dom": "^5.2.0", 13 | "react-scripts": "4.0.3", 14 | "redux": "^4.0.5", 15 | "redux-thunk": "^2.3.0", 16 | "web-vitals": "^1.1.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "redux-logger": "^3.0.6" 44 | }, 45 | "proxy": "http://localhost:5000" 46 | } 47 | -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/AWS-S3-Flask-React/88e486ef51055d12fe0993279c5d6dadace6f941/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/AWS-S3-Flask-React/88e486ef51055d12fe0993279c5d6dadace6f941/react-app/public/logo192.png -------------------------------------------------------------------------------- /react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/AWS-S3-Flask-React/88e486ef51055d12fe0993279c5d6dadace6f941/react-app/public/logo512.png -------------------------------------------------------------------------------- /react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 4 | 5 | import LoginForm from "./components/auth/LoginForm"; 6 | import SignUpForm from "./components/auth/SignUpForm"; 7 | import NavBar from "./components/NavBar"; 8 | import ProtectedRoute from "./components/auth/ProtectedRoute"; 9 | import UsersList from "./components/UsersList"; 10 | import User from "./components/User"; 11 | import UploadFile from './components/UploadFile'; 12 | import { authenticate } from "./store/session"; 13 | 14 | const App = () => { 15 | const dispatch = useDispatch(); 16 | 17 | const [authenticated, setAuthenticated] = useState(false); 18 | const [loaded, setLoaded] = useState(false); 19 | 20 | useEffect(() => { 21 | (async () => { 22 | const user = await dispatch(authenticate()); 23 | if (!user.errors) { 24 | setAuthenticated(true); 25 | } 26 | setLoaded(true); 27 | })(); 28 | }, [dispatch]); 29 | 30 | if (!loaded) { 31 | return null; 32 | } 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | 49 | 50 | 55 | 56 | 57 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |

My Home Page

69 |
70 |
71 |
72 | ); 73 | }; 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /react-app/src/components/FileUpload.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useDispatch } from "react-redux"; 3 | 4 | import { uploadFile } from '../store/files'; 5 | 6 | const FileUpload = () => { 7 | const dispatch = useDispatch(); 8 | 9 | const [file, setFile] = useState(); 10 | const [errors, setErrors] = useState([]); 11 | 12 | const updateFile = (e) => { 13 | // This is for a single file upload. 14 | const file = e.target.files[0]; 15 | if (file) setFile(file); 16 | }; 17 | 18 | const submitHandler = (e) => { 19 | e.preventDefault(); 20 | const fileForm = { 21 | user_id: user.id, 22 | file, 23 | }; 24 | const uploadedFile = await dispatch(uploadFile(fileForm)); 25 | if (!uploadedFile.errors) { 26 | setErrors(uploadedFile.errors); 27 | } 28 | }; 29 | 30 | return ( 31 | <> 32 |
33 | {errors.length > 0 && ( 34 |
    35 | {errors.map(error => ( 36 |
  • {error}
  • 37 | ))} 38 |
39 | )} 40 | 41 | 42 |
43 | 44 | ); 45 | }; 46 | 47 | export default FileUpload; 48 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import LogoutButton from './auth/LogoutButton'; 4 | 5 | const NavBar = ({ setAuthenticated }) => { 6 | return ( 7 | 39 | ); 40 | } 41 | 42 | export default NavBar; 43 | -------------------------------------------------------------------------------- /react-app/src/components/UploadFile.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | 4 | import { uploadFile } from '../store/files'; 5 | 6 | const UploadFile = () => { 7 | const dispatch = useDispatch(); 8 | const user = useSelector((state) => state.session.user); 9 | const uploadedFile = useSelector((state) => state.file); 10 | 11 | const [file, setFile] = useState(); 12 | 13 | const updateFile = (e) => { 14 | setFile(e.target.files[0]); 15 | }; 16 | 17 | const handleSubmit = (e) => { 18 | e.preventDefault(); 19 | const fileForm = { 20 | user_id: user.id, 21 | file 22 | }; 23 | dispatch(uploadFile(fileForm)); 24 | }; 25 | 26 | return ( 27 | <> 28 |
29 | 33 | 34 |
35 | {uploadFile && {uploadedFile.user_id}} 36 | 37 | ); 38 | }; 39 | 40 | export default UploadFile; 41 | -------------------------------------------------------------------------------- /react-app/src/components/User.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useParams } from "react-router-dom"; 4 | 5 | import { getUser } from '../store/users'; 6 | 7 | function User() { 8 | // Notice we use useParams here instead of getting the params 9 | // From props. 10 | const { userId } = useParams(); 11 | const dispatch = useDispatch(); 12 | const user = useSelector((state) => state.users[userId]); 13 | 14 | useEffect(() => { 15 | if (!userId) { 16 | return 17 | } 18 | dispatch(getUser(userId)); 19 | }, [dispatch, userId]); 20 | 21 | if (!user) { 22 | return null; 23 | } 24 | 25 | return ( 26 |
    27 |
  • 28 | User Id {userId} 29 |
  • 30 |
  • 31 | Username {user.username} 32 |
  • 33 |
  • 34 | Email {user.email} 35 |
  • 36 |
37 | ); 38 | } 39 | export default User; 40 | -------------------------------------------------------------------------------- /react-app/src/components/UsersList.js: -------------------------------------------------------------------------------- 1 | import { useEffect, } from "react"; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { NavLink } from "react-router-dom"; 4 | 5 | import { getUsers } from '../store/users'; 6 | 7 | function UsersList() { 8 | const dispatch = useDispatch(); 9 | const users = useSelector((state) => Object.values(state.users)); 10 | 11 | useEffect(() => { 12 | dispatch(getUsers()); 13 | }, [dispatch]); 14 | 15 | const userComponents = users.map((user) => { 16 | return ( 17 |
  • 18 | {user.username} 19 |
  • 20 | ); 21 | }); 22 | 23 | return ( 24 | <> 25 |

    User List:

    26 |
      {userComponents}
    27 | 28 | ); 29 | } 30 | 31 | export default UsersList; 32 | -------------------------------------------------------------------------------- /react-app/src/components/auth/LoginForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { Redirect } from "react-router-dom"; 4 | 5 | import { login } from "../../store/session"; 6 | 7 | const LoginForm = ({ authenticated, setAuthenticated }) => { 8 | const dispatch = useDispatch(); 9 | 10 | const [errors, setErrors] = useState([]); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | 14 | const onLogin = async (e) => { 15 | e.preventDefault(); 16 | const user = await dispatch(login(email, password)); 17 | if (!user.errors) { 18 | setAuthenticated(true); 19 | } else { 20 | setErrors(user.errors); 21 | } 22 | }; 23 | 24 | const updateEmail = (e) => { 25 | setEmail(e.target.value); 26 | }; 27 | 28 | const updatePassword = (e) => { 29 | setPassword(e.target.value); 30 | }; 31 | 32 | if (authenticated) { 33 | return ; 34 | } 35 | 36 | return ( 37 |
    38 |
    39 | {errors.map((error) => ( 40 |
    {error}
    41 | ))} 42 |
    43 |
    44 | 45 | 52 |
    53 |
    54 | 55 | 62 | 63 |
    64 |
    65 | ); 66 | }; 67 | 68 | export default LoginForm; 69 | -------------------------------------------------------------------------------- /react-app/src/components/auth/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | 3 | import { logout } from "../../store/session"; 4 | 5 | const LogoutButton = ({ setAuthenticated }) => { 6 | const dispatch = useDispatch(); 7 | 8 | const onLogout = async (e) => { 9 | await dispatch(logout()); 10 | setAuthenticated(false); 11 | }; 12 | 13 | return ; 14 | }; 15 | 16 | export default LogoutButton; 17 | -------------------------------------------------------------------------------- /react-app/src/components/auth/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import { Route, Redirect } from 'react-router-dom'; 2 | 3 | const ProtectedRoute = props => { 4 | return ( 5 | 6 | {(props.authenticated)? props.children : } 7 | 8 | ) 9 | }; 10 | 11 | 12 | export default ProtectedRoute; 13 | -------------------------------------------------------------------------------- /react-app/src/components/auth/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { Redirect } from "react-router-dom"; 4 | 5 | import { signUp } from "../../store/session"; 6 | 7 | const SignUpForm = ({ authenticated, setAuthenticated }) => { 8 | const dispatch = useDispatch(); 9 | 10 | const [username, setUsername] = useState(""); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [repeatPassword, setRepeatPassword] = useState(""); 14 | 15 | const onSignUp = async (e) => { 16 | e.preventDefault(); 17 | if (password === repeatPassword) { 18 | const user = await dispatch(signUp(username, email, password)); 19 | if (!user.errors) { 20 | setAuthenticated(true); 21 | } 22 | } 23 | }; 24 | 25 | const updateUsername = (e) => { 26 | setUsername(e.target.value); 27 | }; 28 | 29 | const updateEmail = (e) => { 30 | setEmail(e.target.value); 31 | }; 32 | 33 | const updatePassword = (e) => { 34 | setPassword(e.target.value); 35 | }; 36 | 37 | const updateRepeatPassword = (e) => { 38 | setRepeatPassword(e.target.value); 39 | }; 40 | 41 | if (authenticated) { 42 | return ; 43 | } 44 | 45 | return ( 46 |
    47 |
    48 | 49 | 55 |
    56 |
    57 | 58 | 64 |
    65 |
    66 | 67 | 73 |
    74 |
    75 | 76 | 83 |
    84 | 85 |
    86 | ); 87 | }; 88 | 89 | export default SignUpForm; 90 | -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | import configureStore from './store'; 7 | import "./index.css"; 8 | import App from "./App"; 9 | 10 | const store = configureStore(); 11 | 12 | const Root = () => ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | ReactDOM.render( 21 | 22 | 23 | , 24 | document.getElementById("root") 25 | ); 26 | -------------------------------------------------------------------------------- /react-app/src/store/files.js: -------------------------------------------------------------------------------- 1 | const SET_FILE = "files/SET_FILE"; 2 | 3 | const setFile = (file) => { 4 | return { 5 | type: SET_FILE, 6 | file, 7 | }; 8 | }; 9 | 10 | export const uploadFile = (fileForm) => async (dispatch) => { 11 | const { 12 | user_id, 13 | /* all, 14 | other, 15 | form, 16 | fields, */ 17 | file, // this is the file for uploading 18 | } = fileForm; 19 | 20 | const form = new FormData(); 21 | form.append('user_id', user_id); 22 | // repeat as necessary for each required form field 23 | form.append('file', file); 24 | 25 | const res = await fetch('/api/files', { 26 | method: "POST", 27 | body: form, 28 | }); 29 | 30 | const uploadedFile = await res.json(); 31 | if (!uploadedFile.errors) { 32 | dispatch(setFile(uploadedFile)); 33 | } 34 | return uploadedFile; 35 | }; 36 | 37 | const initialState = {}; 38 | 39 | const fileReducer = (state = initialState, action) => { 40 | switch (action.type) { 41 | case SET_FILE: 42 | return action.file; 43 | default: 44 | return state; 45 | } 46 | }; 47 | 48 | export default fileReducer; 49 | -------------------------------------------------------------------------------- /react-app/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import sessionReducer from './session'; 5 | import usersReducer from './users'; 6 | import fileReducer from './files'; 7 | 8 | const rootReducer = combineReducers({ 9 | session: sessionReducer, 10 | users: usersReducer, 11 | file: fileReducer, 12 | }); 13 | 14 | let enhancer; 15 | 16 | if (process.env.NODE_ENV === 'production') { 17 | enhancer = applyMiddleware(thunk); 18 | } else { 19 | const logger = require('redux-logger').default; 20 | const composeEnhancers = 21 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 22 | enhancer = composeEnhancers(applyMiddleware(thunk, logger)); 23 | } 24 | 25 | const configureStore = (preloadedState) => { 26 | return createStore(rootReducer, preloadedState, enhancer); 27 | }; 28 | 29 | export default configureStore; 30 | -------------------------------------------------------------------------------- /react-app/src/store/session.js: -------------------------------------------------------------------------------- 1 | const SET_SESSION = 'session/SET_SESSION'; 2 | const REMOVE_SESSION = 'session/REMOVE_SESSION'; 3 | 4 | const setSession = (user) => { 5 | return { 6 | type: SET_SESSION, 7 | user, 8 | }; 9 | }; 10 | 11 | const removeSession = () => { 12 | return { 13 | type: REMOVE_SESSION, 14 | }; 15 | }; 16 | 17 | export const authenticate = () => async (dispatch) => { 18 | const response = await fetch('/api/auth/',{ 19 | headers: { 20 | 'Content-Type': 'application/json' 21 | } 22 | }); 23 | const user = await response.json(); 24 | if (!user.errors) { 25 | dispatch(setSession(user)); 26 | } 27 | return user; 28 | }; 29 | 30 | export const login = (email, password) => async (dispatch) => { 31 | const response = await fetch('/api/auth/login', { 32 | method: 'POST', 33 | headers: { 34 | 'Content-Type': 'application/json' 35 | }, 36 | body: JSON.stringify({ 37 | email, 38 | password 39 | }) 40 | }); 41 | const user = await response.json(); 42 | if (!user.errors) { 43 | dispatch(setSession(user)); 44 | } 45 | return user; 46 | }; 47 | 48 | export const logout = () => async (dispatch) => { 49 | const response = await fetch("/api/auth/logout", { 50 | headers: { 51 | "Content-Type": "application/json", 52 | } 53 | }); 54 | if (response.ok) { 55 | dispatch(removeSession()); 56 | } 57 | return await response.json(); 58 | }; 59 | 60 | export const signUp = (username, email, password) => async (dispatch) => { 61 | const response = await fetch("/api/auth/signup", { 62 | method: "POST", 63 | headers: { 64 | "Content-Type": "application/json", 65 | }, 66 | body: JSON.stringify({ 67 | username, 68 | email, 69 | password, 70 | }), 71 | }); 72 | const user = await response.json(); 73 | if (!user.errors) { 74 | dispatch(setSession(user)); 75 | } 76 | return user; 77 | }; 78 | 79 | const initialState = { 80 | user: null, 81 | }; 82 | 83 | const sessionReducer = (state = initialState, action) => { 84 | switch (action.type) { 85 | case SET_SESSION: 86 | return { ...state, user: action.user }; 87 | case REMOVE_SESSION: 88 | return { ...state, user: null }; 89 | default: 90 | return state; 91 | } 92 | }; 93 | 94 | export default sessionReducer; 95 | -------------------------------------------------------------------------------- /react-app/src/store/users.js: -------------------------------------------------------------------------------- 1 | const SET_USERS = 'users/SET_USERS'; 2 | const SET_USER = 'users/SET_USER'; 3 | 4 | const setUsers = (users) => ({ 5 | type: SET_USERS, 6 | users, 7 | }); 8 | 9 | const setUser = (user) => ({ 10 | type: SET_USER, 11 | user, 12 | }); 13 | 14 | export const getUser = (id) => async (dispatch) => { 15 | const res = await fetch(`/api/users/${id}`); 16 | const user = await res.json(); 17 | dispatch(setUser(user)); 18 | }; 19 | 20 | export const getUsers = () => async (dispatch) => { 21 | const res = await fetch('/api/users'); 22 | const data = await res.json(); 23 | dispatch(setUsers(data.users)); 24 | }; 25 | 26 | const initialState = {}; 27 | 28 | const usersReducer = (state = initialState, action) => { 29 | switch (action.type) { 30 | case SET_USERS: 31 | return { ...Object.fromEntries(action.users.map((user) => [user.id, user])) }; 32 | case SET_USER: 33 | return { ...state, [action.user.id]: action.user }; 34 | default: 35 | return state; 36 | } 37 | }; 38 | 39 | export default usersReducer; 40 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements 6 | # 7 | 8 | -i https://pypi.org/simple 9 | alembic==1.5.5 10 | boto3==1.17.16 11 | botocore==1.20.16; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' 12 | click==7.1.2 13 | dnspython==2.1.0; python_version >= '3.6' 14 | email-validator==1.1.2 15 | flask-cors==3.0.10 16 | flask-jwt-extended==4.0.2 17 | flask-login==0.5.0 18 | flask-migrate==2.7.0 19 | flask-sqlalchemy==2.4.4 20 | flask-wtf==0.14.3 21 | flask==1.1.2 22 | gunicorn==20.0.4 23 | idna==3.1; python_version >= '3.4' 24 | itsdangerous==1.1.0 25 | jinja2==2.11.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 26 | jmespath==0.10.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' 27 | mako==1.1.4; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 28 | markupsafe==1.1.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 29 | pyjwt==2.0.1 30 | python-dateutil==2.8.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 31 | python-dotenv==0.15.0 32 | python-editor==1.0.4 33 | s3transfer==0.3.4 34 | six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 35 | sqlalchemy==1.3.23 36 | urllib3==1.26.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' 37 | werkzeug==1.0.1 38 | wtforms==2.3.3 39 | --------------------------------------------------------------------------------