├── .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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
24 |
--------------------------------------------------------------------------------
/vue/src/components/message.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
“{{message.message}}”
6 |
{{message.name}}
7 |
8 |
20 |
21 |
22 |
23 |
24 |
37 |
--------------------------------------------------------------------------------
/vue/src/components/messageNew.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
42 |
--------------------------------------------------------------------------------
/vue/src/components/navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
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 |
2 |
3 |
This is an about page
4 |
5 |
6 |
7 |
8 |
44 |
--------------------------------------------------------------------------------
/vue/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
39 |
--------------------------------------------------------------------------------
/vue/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
51 |
--------------------------------------------------------------------------------
/vue/src/views/LoginOld.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
32 |
33 |
34 |
96 |
--------------------------------------------------------------------------------
/vue/src/views/template.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is an about page
4 |
5 |
6 |
7 |
8 |