├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── __init__.py ├── main.py ├── routers │ ├── __init__.py │ ├── messages.py │ └── users.py └── services │ ├── __init__.py │ ├── messages.py │ ├── redis.py │ ├── sqlite.py │ ├── users.py │ └── util.py ├── data └── schema.sql ├── requirements.txt └── vue ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── components │ ├── message.vue │ ├── messageNew.vue │ └── navbar.vue ├── main.js ├── router.js ├── store.js └── views │ ├── About.vue │ ├── Home.vue │ ├── Login.vue │ ├── LoginOld.vue │ └── template.vue └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | static/ 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Terraform 133 | .terraform 134 | terraform.tfstate 135 | terraform/terraform.tfstate.backup 136 | 137 | # Local Data 138 | sqlite.db 139 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/usr/bin/python3" 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest AS build 2 | COPY ./vue /vue 3 | WORKDIR /vue 4 | RUN npm install && npm run build 5 | 6 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 7 | RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists 8 | COPY requirements.txt / 9 | RUN pip install -r /requirements.txt 10 | COPY --from=build /vue/dist /vue/dist 11 | COPY ./app /app/app 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Will Fong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker + FastAPI + Vue 2 | 3 | Technologies: 4 | - Docker 5 | - Python FastAPI 6 | - Vue JS 7 | 8 | ## Getting Started 9 | 10 | This guide assumes basic understanding of Docker, Python, and Javascript. 11 | 12 | **Getting the demo application up and running:** 13 | 14 | 1. Create a new repo using this as a template: https://github.com/willfong/docker-fastapi-vue/generate 15 | 1. Clone the new repo locally: `git clone git@github.com:willfong/test-repo.git` 16 | 1. Change to the new repo: `cd test-repo` 17 | 1. Create a `.env` file and add session secret: 18 | 19 | ``` 20 | JWT_SIGNING_TOKEN=SOME_SECRET_HERE 21 | 1. Build the Docker image: `docker build --tag dockerfastapivue .` 22 | 1. Start a container named `app` from the image created above: 23 | 24 | ``` 25 | docker run \ 26 | --rm -d \ 27 | --name app \ 28 | -p 5000:80 \ 29 | -v ${PWD}/data:/data \ 30 | --env-file .env \ 31 | dockerfastapivue 32 | 1. Check to make sure the `app` container is still running: `docker ps` 33 | 1. Create the SQLite datafile: `docker exec -it app sqlite3 /data/sqlite.db ".read /data/schema.sql"` 34 | 1. Check the SQLite datafile to ensure there are tables: `docker exec -it app sqlite3 /data/sqlite.db .schema` 35 | 1. Open a web browser to: http://localhost:5000/ 36 | 1. Click "Login" in the top right corner 37 | 1. Click "Test Account Login" and enter in any username. 38 | 1. Add a new message and see the message displayed. 39 | 40 | **Make changes to the backend system:** 41 | 42 | 1. Check the logs from the backend: `docker logs app` 43 | 1. In `app/main.py` on line 16, add: 44 | 45 | ``` 46 | @app.get("/echo/:message") 47 | def echo(message: str): 48 | util.logger.warning(f"Message: {message}") 49 | return {"msg": message} 50 | 1. Stop the Docker container: `docker stop app` 51 | 1. Rebuild Docker image: `docker build --tag dockerfastapivue .` 52 | 1. Start a new container with the new image: 53 | 54 | ``` 55 | docker run \ 56 | --rm -d \ 57 | --name app \ 58 | -p 5000:80 \ 59 | -v ${PWD}/data:/data \ 60 | dockerfastapivue 61 | 1. Test the new endpoint: `curl localhost:5000/echo/hello-world` 62 | 1. Check the Docker logs for your test message: `docker logs app` 63 | 64 | **Make changes to the frontend system:** 65 | 66 | 1. Change to the `vue` directory: `cd vue` 67 | 1. Install the Javascript dependencies: `npm install` 68 | 1. In `src/components/navbar.vue`, change: `

Example App

` to `

Hello App!

` 69 | 1. Build the production distribution: `npm run build` 70 | 1. Stop the existing Docker container: `docker stop app` 71 | 1. Start a new container with the new image: 72 | 73 | ``` 74 | docker run \ 75 | --rm -d \ 76 | --name app \ 77 | -p 5000:80 \ 78 | -v ${PWD}:/vue \ 79 | -v ${PWD}/data:/data \ 80 | dockerfastapivue 81 | 1. Open a web browser to: http://localhost:5000 and verify 82 | 83 | 84 | ## Docker Commands 85 | 86 | Create image locally: 87 | ``` 88 | docker build --tag dockerfastapivue . 89 | ``` 90 | 91 | Run an instance: 92 | ``` 93 | docker run \ 94 | --rm -d \ 95 | --name app \ 96 | -p 5000:80 \ 97 | -v ${PWD}/vue:/vue \ 98 | -v ${PWD}/data:/data \ 99 | 100 | dockerfastapivue 101 | ``` 102 | 103 | Access the database directly: 104 | ``` 105 | docker exec -it app sqlite3 /data/sqlite.db 106 | ``` 107 | 108 | ## GitHub Auth Flow 109 | 110 | GitHub OAuth is a bit easier to enable than Facebook and Google OAuth. 111 | 112 | 1. Create a GitHub OAuth Application: https://github.com/settings/applications/new 113 | 1. Application Name and Homepage URL are just for display. Set Authorization callback URL to `http://localhost:5000/oauth/github` 114 | 1. Add the following to the `.env` file: 115 | 116 | ``` 117 | GITHUB_CLIENT_ID=626...1d2 118 | GITHUB_CLIENT_SECRET=cc3...350 119 | 1. Pass the `.env` file to Docker when you create the instance: 120 | 121 | ``` 122 | docker run \ 123 | --rm -d \ 124 | --name app \ 125 | -p 5000:80 \ 126 | -v ${PWD}/vue:/vue \ 127 | -v ${PWD}/data:/data \ 128 | --env-file .env \ 129 | dockerfastapivue 130 | 1. You can use the GitHub login button now. 131 | 132 | Details about the user profile passed back from GitHub: https://developer.github.com/v3/users/#get-the-authenticated-user 133 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willfong/docker-fastapi-vue/6deefbe80f06bd3c7350c46a2a3875fe7b76612f/app/__init__.py -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from .routers import users, messages 3 | from .services import util 4 | from starlette.requests import Request 5 | from starlette.staticfiles import StaticFiles 6 | from starlette.responses import RedirectResponse, JSONResponse, HTMLResponse 7 | 8 | app = FastAPI() 9 | 10 | # This is only really for serving test files. We would probably serve static 11 | # files from S3 directly. 12 | app.mount("/static", StaticFiles(directory="/vue/dist"), name="static") 13 | 14 | app.include_router(users.router, prefix="/api/users") 15 | app.include_router(messages.router, prefix="/api/messages") 16 | 17 | 18 | @app.get("/.*", include_in_schema=False) 19 | def root(): 20 | with open('/vue/dist/index.html') as f: 21 | return HTMLResponse(content=f.read(), status_code=200) 22 | 23 | 24 | @app.get("/log-output-test") 25 | def log_output_test(): 26 | util.logger.debug("logging debug") 27 | util.logger.info("logging info") 28 | util.logger.warn("logging warning") 29 | util.logger.error("logging error") 30 | return {"msg": "Logging output"} -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willfong/docker-fastapi-vue/6deefbe80f06bd3c7350c46a2a3875fe7b76612f/app/routers/__init__.py -------------------------------------------------------------------------------- /app/routers/messages.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Header, HTTPException 2 | from pydantic import BaseModel 3 | from ..services import util, users, messages 4 | 5 | router = APIRouter() 6 | 7 | @router.get("/") 8 | def get(): 9 | return messages.all() 10 | 11 | class Message(BaseModel): 12 | text: str 13 | 14 | @router.post("/add") 15 | def add(message: Message, authorization: str = Header(None)): 16 | user_detail = users.get_user_data_from_token(authorization) 17 | if not user_detail: 18 | raise HTTPException(status_code=403, detail="Invalid Authentication Token") 19 | response = messages.add(user_detail.get('id'), message.text) 20 | return {"msg": response} 21 | -------------------------------------------------------------------------------- /app/routers/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Header, HTTPException 2 | from pydantic import BaseModel 3 | from ..services import util, users 4 | 5 | router = APIRouter() 6 | 7 | class LoginToken(BaseModel): 8 | value: str 9 | 10 | @router.post("/facebook") 11 | def login_facebook(token: LoginToken): 12 | facebook_data = users.facebook_verify_access_token(token.value) 13 | if not facebook_data: 14 | raise HTTPException(status_code=403, detail="Invalid Facebook Token") 15 | user_id = users.find_or_create_user('facebook', facebook_data['id'], facebook_data) 16 | return {"token": users.create_login_token(user_id)} 17 | 18 | @router.post("/google") 19 | def login_google(token: LoginToken): 20 | google_data = users.google_verify_access_token(token.value) 21 | if not google_data: 22 | raise HTTPException(status_code=403, detail="Invalid Google Token") 23 | user_id = users.find_or_create_user('google', google_data['sub'], google_data) 24 | return {"token": users.create_login_token(user_id)} 25 | 26 | @router.post("/test-account") 27 | def login_test(username: LoginToken): 28 | user_id = users.find_or_create_user(f"test-account||{username.value}") 29 | util.logger.warning(f"Test Account Logged In: {user_id}") 30 | if not user_id: 31 | raise HTTPException(status_code=403, detail="Invalid Authentication Token") 32 | return {"token": users.create_login_token(user_id)} 33 | 34 | @router.post("/github") 35 | def login_github(token: LoginToken): 36 | profile = users.github_login(token.value) 37 | if not profile: 38 | raise HTTPException(status_code=403, detail="Invalid Authentication Token") 39 | user_id = users.find_or_create_user(f"github||{profile.get('id')}") 40 | util.logger.warning(f"GitHub Account Logged In: {user_id} ({profile.get('id')})") 41 | if not user_id: 42 | raise HTTPException(status_code=403, detail="Invalid Authentication Token") 43 | return {"token": users.create_login_token(user_id)} 44 | 45 | @router.get("/lookup") 46 | def lookup(id: str): 47 | return users.lookup(id) -------------------------------------------------------------------------------- /app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willfong/docker-fastapi-vue/6deefbe80f06bd3c7350c46a2a3875fe7b76612f/app/services/__init__.py -------------------------------------------------------------------------------- /app/services/messages.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime 3 | from ..services import util, sqlite 4 | 5 | def add(users_id, message_text): 6 | query = "INSERT INTO messages (created_at, users_id, message) VALUES (?,?,?)" 7 | params = (datetime.utcnow().isoformat(), users_id, message_text) 8 | if sqlite.write(query, params): 9 | return True 10 | return False 11 | 12 | def all(): 13 | query = "SELECT m.id, m.created_at, m.message, u.name FROM messages AS m INNER JOIN users AS u ON m.users_id = u.id ORDER BY m.created_at DESC" 14 | return sqlite.read(query) 15 | -------------------------------------------------------------------------------- /app/services/redis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import redis 3 | import json 4 | from ..services import util 5 | 6 | r = redis.Redis(host=os.environ.get('REDIS_ENDPOINT_URL')) 7 | 8 | def put(key, value, ttl): 9 | if r.set(key, json.dumps(value), ex=ttl): 10 | return True 11 | return False 12 | 13 | def get(k): 14 | results = r.get(k) 15 | if results: 16 | return json.loads(results) 17 | return False 18 | 19 | def incr(k): 20 | if r.incr(k): 21 | return True 22 | return False 23 | 24 | # TODO: scan shouldn't be used. Needs to be upgraded 25 | def scan(): 26 | return r.scan() 27 | -------------------------------------------------------------------------------- /app/services/sqlite.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | from ..services import util 4 | 5 | def dict_factory(cursor, row): 6 | d = {} 7 | for idx, col in enumerate(cursor.description): 8 | d[col[0]] = row[idx] 9 | return d 10 | 11 | def db_connect(): 12 | conn = sqlite3.connect('/data/sqlite.db') 13 | conn.row_factory = dict_factory 14 | return conn 15 | 16 | def read(query, params=None, one=False): 17 | try: 18 | conn = db_connect() 19 | cur = conn.cursor() 20 | if params: 21 | cur.execute(query, params) 22 | else: 23 | cur.execute(query) 24 | if one: 25 | return cur.fetchone() 26 | return cur.fetchall() 27 | except sqlite3.Error as e: 28 | util.logger.error(f"[SQLITE READ ERROR] {e.args[0]}") 29 | return False 30 | 31 | def write(query, params=None, lastrowid=False): 32 | try: 33 | conn = db_connect() 34 | cur = conn.cursor() 35 | if cur.execute(query, params): 36 | conn.commit() 37 | if lastrowid: 38 | return cur.lastrowid 39 | return True 40 | return False 41 | except sqlite3.Error as e: 42 | util.logger.error(f"[SQLITE WRITE ERROR] {e.args[0]}") 43 | return False 44 | -------------------------------------------------------------------------------- /app/services/users.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | import requests 4 | import json 5 | import jwt 6 | from datetime import datetime, timedelta 7 | from ..services import util, sqlite 8 | 9 | def get_details(id): 10 | query = "SELECT * FROM users WHERE id = ?" 11 | params = (id,) 12 | return sqlite.read(query, params, one=True) 13 | 14 | def lookup(oauth): 15 | query = "SELECT * FROM users WHERE oauth = ?" 16 | params = (oauth,) 17 | return sqlite.read(query, params, one=True) 18 | 19 | def create_login_token(sub): 20 | return jwt.encode({ 21 | 'sub': sub, 22 | 'iat': datetime.utcnow(), 23 | 'exp': datetime.utcnow() + timedelta(minutes=60*24*30) 24 | }, get_secret_token()) 25 | 26 | def get_user_data_from_token(token): 27 | token_dict = verify_token(token) 28 | if not token_dict: 29 | util.logger.error(f'Could not verify token: {token}') 30 | return False 31 | return lookup(token_dict.get('sub')) 32 | 33 | def get_secret_token(): 34 | return os.environ.get('JWT_SIGNING_TOKEN') 35 | 36 | def verify_token(token): 37 | try: 38 | response = jwt.decode(token, get_secret_token()) 39 | except: 40 | util.logger.error(f'Bad token: {token}') 41 | return False 42 | return response 43 | 44 | def find_or_create_user(oauth): 45 | user_hash = hashlib.sha224(oauth.encode('ascii')).hexdigest() 46 | query = "INSERT INTO users (oauth, last_login) VALUES (?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ON CONFLICT (oauth) DO UPDATE SET last_login = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')" 47 | params = (user_hash,) 48 | if not sqlite.write(query, params): 49 | return False 50 | return user_hash 51 | 52 | def github_login(token): 53 | auth_response = github_token_to_access_code(token) 54 | if not auth_response: 55 | return False 56 | user_profile = github_get_user_profile(auth_response.get('access_token')) 57 | if not user_profile: 58 | return False 59 | return user_profile 60 | 61 | def github_token_to_access_code(token): 62 | payload = { 63 | 'client_id': os.environ.get('GITHUB_CLIENT_ID'), 64 | 'client_secret': os.environ.get('GITHUB_CLIENT_SECRET'), 65 | 'code': token 66 | } 67 | response = requests.post("https://github.com/login/oauth/access_token", data=payload, headers={'Accept': 'application/json'}) 68 | if response.status_code != 200: 69 | return False 70 | return response.json() 71 | 72 | def github_get_user_profile(oauth_token): 73 | response = requests.get("https://api.github.com/user", headers={'Authorization': f"token {oauth_token}"}) 74 | if response.status_code != 200: 75 | return False 76 | return response.json() 77 | 78 | 79 | def google_verify_access_token(id_token): 80 | # We're doing it the lazy way here. What we get from the client side is JWT, we can just verify that instead of calling Google 81 | # Reason for that is to reduce the amount of dependencies for this, a demo app 82 | # For production, we should do it the right way by using google-auth 83 | 84 | response = requests.get(f'https://oauth2.googleapis.com/tokeninfo?id_token={id_token}').json() 85 | if response.get('error'): 86 | errmsg = response.get('error_description') 87 | util.logger.error(f"[USER|google_verify_access_token] {errmsg}") 88 | return False 89 | # Here, you should check that your domain name is in hd 90 | # if jwt['hd'] == 'example.com': 91 | # return jwt 92 | # For now, we're just going to accept all 93 | return response 94 | 95 | 96 | FACEBOOK_URL_APP_TOKEN = f'https://graph.facebook.com/oauth/access_token?client_id={os.environ.get("FACEBOOK_CLIENT_ID")}&client_secret={os.environ.get("FACEBOOK_CLIENT_SECRET")}&grant_type=client_credentials' 97 | def facebook_get_app_token(): 98 | return requests.get(FACEBOOK_URL_APP_TOKEN).json()['access_token'] 99 | 100 | def facebook_verify_access_token(access_token): 101 | app_token = facebook_get_app_token() 102 | access_token_url = f'https://graph.facebook.com/debug_token?input_token={access_token}&access_token={app_token}' 103 | try: 104 | debug_token = requests.get(access_token_url).json()['data'] 105 | except (ValueError, KeyError, TypeError) as error: 106 | util.logger.error(f"[USER|facebook_verify_access_token] {error}") 107 | return error 108 | user_data_url = f"https://graph.facebook.com/{debug_token['user_id']}/?&access_token={app_token}" 109 | user_data = requests.get(user_data_url).json() 110 | return user_data 111 | 112 | ''' 113 | def find_or_create_user(oauth_source, user_id, oauth_payload): 114 | user_plaintext = f"{oauth_source}|{user_id}" 115 | user_hash = hashlib.sha224(user_plaintext.encode('ascii')).hexdigest() 116 | query = "INSERT OR IGNORE INTO users (userhash, source, payload) VALUES (?,?,?)" 117 | params = (user_hash, oauth_source, json.dumps(oauth_payload)) 118 | if sqlite.write(query, params): 119 | return user_hash 120 | else: 121 | return False 122 | ''' 123 | 124 | 125 | -------------------------------------------------------------------------------- /app/services/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | myFormatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') 6 | handler = logging.StreamHandler() 7 | handler.setFormatter(myFormatter) 8 | logger.addHandler(handler) 9 | -------------------------------------------------------------------------------- /data/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS messages; 2 | CREATE TABLE messages ( 3 | id INTEGER PRIMARY KEY, 4 | created_at TEXT, 5 | users_id INT, 6 | message TEXT 7 | ); 8 | 9 | DROP TABLE IF EXISTS users; 10 | CREATE TABLE users ( 11 | id INTEGER PRIMARY KEY, 12 | oauth TEXT, 13 | admin BOOLEAN, 14 | name TEXT, 15 | last_login TEXT, 16 | UNIQUE(oauth) 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.5.0 2 | boto3==1.13.6 3 | botocore==1.16.6 4 | certifi==2020.4.5.1 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==3.3.2 9 | docutils==0.15 10 | fastapi==0.54.1 11 | gunicorn==20.0.4 12 | h11==0.9.0 13 | httptools==0.1.1 14 | idna==2.9 15 | jmespath==0.9.5 16 | jwt==1.0.0 17 | pycparser==2.20 18 | pydantic==1.5.1 19 | PyJWT==2.4.0 20 | python-dateutil==2.8.1 21 | python-dotenv==0.13.0 22 | redis==3.5.1 23 | requests==2.23.0 24 | s3transfer==0.3.3 25 | six==1.14.0 26 | starlette==0.13.2 27 | statsd==3.3.0 28 | urllib3==1.25.9 29 | uuid==1.30 30 | uvicorn==0.11.7 31 | uvloop==0.14.0 32 | websockets==8.1 -------------------------------------------------------------------------------- /vue/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /vue/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Will Fong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vue/README.md: -------------------------------------------------------------------------------- 1 | # Vue + Facebook Login 2 | 3 | ## How To Use 4 | 5 | 1. Download 6 | 1. Replace Facebook App ID with your own 7 | 1. Customize anything else 8 | 1. `npm run build` 9 | 1. Copy `dist/*` to your own backend 10 | 11 | 12 | ## Login Process 13 | 14 | 1. Call to FB Login 15 | 1. Retrieve FB Access Token (FBAT) 16 | 1. Call backend login/ with FBAT for verification 17 | 1. Backend verifies FBAT with FB 18 | 1. Backend sends JSON Web Token (JWT) 19 | 1. Call backend api/ with JWT Authorization header 20 | 21 | 22 | ## Set up Facebook Login 23 | 24 | https://developers.facebook.com/ 25 | 26 | Settings -> Basic -> Add Platform 27 | 28 | Website -> Callback URL: http://localhost:8080/auth/facebook/callback 29 | 30 | `auth/facebook/callback` will be handled by Vue frontend. 31 | 32 | 33 | ## Warnings 34 | 35 | `The method FB.login can no longer be called from http pages.` 36 | 37 | https://developers.facebook.com/blog/post/2018/06/08/enforce-https-facebook-login/ 38 | You will still be able to use HTTP with “localhost” addresses, but only while your app is still in development mode. 39 | 40 | -------------------------------------------------------------------------------- /vue/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 12 | "@fortawesome/free-brands-svg-icons": "^5.13.0", 13 | "@fortawesome/free-regular-svg-icons": "^5.13.0", 14 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 15 | "@fortawesome/vue-fontawesome": "^0.1.9", 16 | "axios": "^0.21.2", 17 | "bulma": "^0.8.2", 18 | "core-js": "^3.6.5", 19 | "moment": "^2.29.4", 20 | "regenerator-runtime": "^0.13.3", 21 | "vue": "^2.6.10", 22 | "vue-facebook-login-component": "^1.5.0", 23 | "vue-google-signin-button": "^1.0.4", 24 | "vue-router": "^3.1.3", 25 | "vuex": "^3.4.0" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "^4.3.1", 29 | "@vue/cli-plugin-eslint": "^4.3.1", 30 | "@vue/cli-plugin-router": "^4.3.1", 31 | "@vue/cli-plugin-vuex": "^4.3.1", 32 | "@vue/cli-service": "^4.3.1", 33 | "babel-eslint": "^10.0.3", 34 | "eslint": "^5.16.0", 35 | "eslint-plugin-vue": "^5.0.0", 36 | "vue-template-compiler": "^2.6.10" 37 | }, 38 | "eslintConfig": { 39 | "root": true, 40 | "env": { 41 | "node": true 42 | }, 43 | "extends": [ 44 | "plugin:vue/essential", 45 | "eslint:recommended" 46 | ], 47 | "rules": {}, 48 | "parserOptions": { 49 | "parser": "babel-eslint" 50 | } 51 | }, 52 | "browserslist": [ 53 | "> 1%", 54 | "last 2 versions" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willfong/docker-fastapi-vue/6deefbe80f06bd3c7350c46a2a3875fe7b76612f/vue/public/favicon.ico -------------------------------------------------------------------------------- /vue/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | -------------------------------------------------------------------------------- /vue/src/components/message.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /vue/src/components/messageNew.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /vue/src/components/navbar.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 37 | 38 | -------------------------------------------------------------------------------- /vue/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import 'bulma/css/bulma.css' 6 | import GSignInButton from 'vue-google-signin-button' 7 | import { library } from '@fortawesome/fontawesome-svg-core' 8 | import { faUserSecret } from '@fortawesome/free-solid-svg-icons' 9 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 10 | import moment from 'moment'; 11 | 12 | 13 | // See: https://github.com/FortAwesome/vue-fontawesome 14 | library.add(faUserSecret) 15 | 16 | Vue.component('font-awesome-icon', FontAwesomeIcon) 17 | 18 | Vue.config.productionTip = false 19 | Vue.config.devtools = true 20 | Vue.use(GSignInButton) 21 | 22 | Vue.prototype.moment = moment; 23 | 24 | 25 | new Vue({ 26 | router, 27 | store, 28 | render: h => h(App) 29 | }).$mount('#app') 30 | -------------------------------------------------------------------------------- /vue/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '@/views/Home.vue' 4 | import About from '@/views/About.vue' 5 | import Login from '@/views/Login.vue' 6 | 7 | Vue.use(VueRouter); 8 | 9 | const routes = [ 10 | { 11 | path: '/', 12 | name: 'home', 13 | component: Home 14 | }, 15 | { 16 | path: '/about', 17 | name: 'about', 18 | component: About 19 | }, 20 | { 21 | path: '/login', 22 | name: 'login', 23 | component: Login 24 | }, 25 | ] 26 | 27 | export default new VueRouter({routes}) 28 | -------------------------------------------------------------------------------- /vue/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import axios from "axios"; 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | state: { 9 | jwt: false, 10 | }, 11 | mutations: { 12 | JWT_SET(state, jwt) { 13 | state.jwt = jwt; 14 | }, 15 | }, 16 | actions: { 17 | jwtSet({commit}, jwt) { 18 | axios.defaults.headers.common['Authorization'] = jwt; 19 | commit('JWT_SET', jwt); 20 | }, 21 | }, 22 | getters: { 23 | loggedIn: state => state.jwt, 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /vue/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 44 | -------------------------------------------------------------------------------- /vue/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | -------------------------------------------------------------------------------- /vue/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 51 | -------------------------------------------------------------------------------- /vue/src/views/LoginOld.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 33 | 34 | 96 | -------------------------------------------------------------------------------- /vue/src/views/template.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |