You've already forked flask-mongo-api-boilerplate
mirror of
https://github.com/LukePeters/flask-mongo-api-boilerplate.git
synced 2026-05-05 18:01:10 +09:00
First push to GitHub
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.vscode
|
||||
.DS_Store
|
||||
*.pyc
|
||||
Pipfile.lock
|
||||
api/main/config/*.cfg
|
||||
16
Pipfile
Normal file
16
Pipfile
Normal file
@@ -0,0 +1,16 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
flask = "*"
|
||||
flask-cors = "*"
|
||||
pymongo = "*"
|
||||
python-jose = "*"
|
||||
passlib = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## Requirements
|
||||
|
||||
- pipenv
|
||||
- MongoDB
|
||||
- Python 3 (defaults to Python 3.7, but you can change this in the Pipfile before setup)
|
||||
|
||||
## Setup instructions
|
||||
|
||||
1. Clone this repo to your local web server
|
||||
2. `cd` into the directory within the terminal
|
||||
3. Run `./setup.sh` to setup pipenv and configure the Flask app
|
||||
|
||||
## Running the app
|
||||
|
||||
1. Run `pipenv shell` to activate the virtual environment
|
||||
2. Run `./run.sh` to start the Flask application
|
||||
|
||||
## Further configuration
|
||||
|
||||
You can configure the app manually by editing the `api/main/config/config.cfg` file.
|
||||
|
||||
## Auth tokens
|
||||
|
||||
There is a very basic front-end example in place within the `/web` directory. It demonstrates making a few API calls (User Add and User Login).
|
||||
|
||||
A successful login request will return two tokens: `AccessToken` and `RefreshToken`. These should be saved to localStorage and used to set the `AccessToken` and `RefreshToken` request headers for all protected routes (e.g. `GET /user/`).
|
||||
|
||||
You can refresh the `AccessToken` when it returns as expired by submitting a request to `GET /user/auth/`.
|
||||
|
||||
## Notes
|
||||
|
||||
Please excuse the brief instructions. I've only run this in my own environment (MacOS, Python 3.7, MongoDB 4.0.4, pipenv 2018.11.14) so it may not run out of the box on your computer, but I'd be happy to help debug if you get stuck. Reach out on Twitter [@MoonlightLuke](https://twitter.com/MoonlightLuke)
|
||||
38
api/main/__init__.py
Normal file
38
api/main/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from flask import Flask, request
|
||||
from flask_cors import CORS
|
||||
from pymongo import MongoClient
|
||||
from main.tools import JsonResp
|
||||
from jose import jwt
|
||||
import os
|
||||
|
||||
# Import Routes
|
||||
from main.user.routes import user_blueprint
|
||||
|
||||
def create_app():
|
||||
|
||||
# Flask Config
|
||||
app = Flask(__name__)
|
||||
app.config.from_pyfile("config/config.cfg")
|
||||
cors = CORS(app, resources={r"/*": { "origins": app.config["FRONTEND_DOMAIN"] }})
|
||||
|
||||
# Misc Config
|
||||
os.environ["TZ"] = app.config["TIMEZONE"]
|
||||
|
||||
# Database Config
|
||||
if app.config["ENVIRONMENT"] == "development":
|
||||
mongo = MongoClient(app.config["MONGO_HOSTNAME"], app.config["MONGO_PORT"])
|
||||
app.db = mongo[app.config["MONGO_APP_DATABASE"]]
|
||||
else:
|
||||
mongo = MongoClient("localhost")
|
||||
mongo[app.config["MONGO_AUTH_DATABASE"]].authenticate(app.config["MONGO_AUTH_USERNAME"], app.config["MONGO_AUTH_PASSWORD"])
|
||||
app.db = mongo[app.config["MONGO_APP_DATABASE"]]
|
||||
|
||||
# Register Blueprints
|
||||
app.register_blueprint(user_blueprint, url_prefix="/user")
|
||||
|
||||
# Index Route
|
||||
@app.route("/")
|
||||
def index():
|
||||
return JsonResp({ "status": "Online" }, 200)
|
||||
|
||||
return app
|
||||
65
api/main/auth/__init__.py
Normal file
65
api/main/auth/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from flask import current_app as app
|
||||
from flask import request
|
||||
from functools import wraps
|
||||
from main.tools import JsonResp
|
||||
from jose import jwt
|
||||
import datetime
|
||||
|
||||
# Auth Decorator
|
||||
def token_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
access_token = request.headers.get('AccessToken')
|
||||
|
||||
try:
|
||||
data = jwt.decode(access_token, app.config['SECRET_KEY'])
|
||||
except Exception as e:
|
||||
return JsonResp({ "message": "Token is invalid", "exception": str(e) }, 401)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
def encodeAccessToken(user_id, email, plan):
|
||||
|
||||
accessToken = jwt.encode({
|
||||
"user_id": user_id,
|
||||
"email": email,
|
||||
"plan": plan,
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15) # The token will expire in 15 minutes
|
||||
}, app.config["SECRET_KEY"], algorithm="HS256")
|
||||
|
||||
return accessToken
|
||||
|
||||
def encodeRefreshToken(user_id, email, plan):
|
||||
|
||||
refreshToken = jwt.encode({
|
||||
"user_id": user_id,
|
||||
"email": email,
|
||||
"plan": plan,
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(weeks=4) # The token will expire in 4 weeks
|
||||
}, app.config["SECRET_KEY"], algorithm="HS256")
|
||||
|
||||
return refreshToken
|
||||
|
||||
def refreshAccessToken(refresh_token):
|
||||
|
||||
# If the refresh_token is still valid, create a new access_token and return it
|
||||
try:
|
||||
user = app.db.users.find_one({ "refresh_token": refresh_token }, { "_id": 0, "id": 1, "email": 1, "plan": 1 })
|
||||
|
||||
if user:
|
||||
decoded = jwt.decode(refresh_token, app.config["SECRET_KEY"])
|
||||
new_access_token = encodeAccessToken(decoded["user_id"], decoded["email"], decoded["plan"])
|
||||
result = jwt.decode(new_access_token, app.config["SECRET_KEY"])
|
||||
result["new_access_token"] = new_access_token
|
||||
resp = JsonResp(result, 200)
|
||||
else:
|
||||
result = { "message": "Auth refresh token has expired" }
|
||||
resp = JsonResp(result, 403)
|
||||
|
||||
except:
|
||||
result = { "message": "Auth refresh token has expired" }
|
||||
resp = JsonResp(result, 403)
|
||||
|
||||
return resp
|
||||
21
api/main/config/config.cfg.sample
Normal file
21
api/main/config/config.cfg.sample
Normal file
@@ -0,0 +1,21 @@
|
||||
# Config files are not tracked in Git and must be placed manually in each
|
||||
# app environment (e.g. development, staging, production).
|
||||
|
||||
# General
|
||||
DEBUG = True
|
||||
TIMEZONE = "US/Eastern"
|
||||
SECRET_KEY = ""
|
||||
ENVIRONMENT = "development"
|
||||
FLASK_DIRECTORY = "##FLASK_DIRECTORY##"
|
||||
FLASK_DOMAIN = "##FLASK_DOMAIN##"
|
||||
FLASK_PORT = ##FLASK_PORT##
|
||||
FRONTEND_DOMAIN = "##FRONTEND_DOMAIN##"
|
||||
HTTP_HTTPS = "http://"
|
||||
|
||||
# Database
|
||||
MONGO_HOSTNAME = "##MONGO_HOSTNAME##"
|
||||
MONGO_PORT = 27017
|
||||
MONGO_AUTH_DATABASE = ""
|
||||
MONGO_AUTH_USERNAME = ""
|
||||
MONGO_AUTH_PASSWORD = ""
|
||||
MONGO_APP_DATABASE = "##MONGO_APP_DATABASE##"
|
||||
43
api/main/tools/__init__.py
Normal file
43
api/main/tools/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
def nowEpoch():
|
||||
return int(time.time()) * 1000
|
||||
|
||||
def JsonResp(data, status):
|
||||
from flask import Response
|
||||
import json
|
||||
return Response(json.dumps(data), mimetype="application/json", status=status)
|
||||
|
||||
def randID():
|
||||
randId = randString(3) + randString(3) + randString(3) + randString(3) + randString(3) + randString(3)
|
||||
return randId
|
||||
|
||||
def randString(length):
|
||||
randString = ""
|
||||
for _ in range(length):
|
||||
randString += random.choice("AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890")
|
||||
|
||||
return randString
|
||||
|
||||
def randStringCaps(length):
|
||||
randString = ""
|
||||
for _ in range(length):
|
||||
randString += random.choice("ABCDEFGHJKLMNPQRSTUVWXYZ23456789")
|
||||
|
||||
return randString
|
||||
|
||||
def randStringNumbersOnly(length):
|
||||
randString = ""
|
||||
for _ in range(length):
|
||||
randString += random.choice("23456789")
|
||||
|
||||
return randString
|
||||
|
||||
def validEmail(email):
|
||||
import re
|
||||
|
||||
if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$", email) != None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
0
api/main/user/__init__.py
Normal file
0
api/main/user/__init__.py
Normal file
152
api/main/user/models.py
Normal file
152
api/main/user/models.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from flask import current_app as app
|
||||
from flask import Flask, request
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
from jose import jwt
|
||||
from main import tools
|
||||
from main import auth
|
||||
import json
|
||||
|
||||
class User:
|
||||
|
||||
def __init__(self):
|
||||
self.defaults = {
|
||||
"id": tools.randID(),
|
||||
"ip_addresses": [request.remote_addr],
|
||||
"acct_active": True,
|
||||
"date_created": tools.nowEpoch(),
|
||||
"last_login": tools.nowEpoch(),
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"email": "",
|
||||
"plan": "free"
|
||||
}
|
||||
|
||||
def get(self):
|
||||
token_data = jwt.decode(request.headers.get('AccessToken'), app.config['SECRET_KEY'])
|
||||
|
||||
user = app.db.users.find_one({ "id": token_data['user_id'] }, {
|
||||
"_id": 0,
|
||||
"password": 0
|
||||
})
|
||||
|
||||
if user:
|
||||
resp = tools.JsonResp(user, 200)
|
||||
else:
|
||||
resp = tools.JsonResp({ "message": "User not found" }, 404)
|
||||
|
||||
return resp
|
||||
|
||||
def getAuth(self):
|
||||
access_token = request.headers.get("AccessToken")
|
||||
refresh_token = request.headers.get("RefreshToken")
|
||||
|
||||
resp = tools.JsonResp({ "message": "User not logged in" }, 401)
|
||||
|
||||
if access_token:
|
||||
try:
|
||||
decoded = jwt.decode(access_token, app.config["SECRET_KEY"])
|
||||
resp = tools.JsonResp(decoded, 200)
|
||||
except:
|
||||
# If the access_token has expired, get a new access_token - so long as the refresh_token hasn't expired yet
|
||||
resp = auth.refreshAccessToken(refresh_token)
|
||||
|
||||
return resp
|
||||
|
||||
def login(self):
|
||||
resp = tools.JsonResp({ "message": "Invalid user credentials" }, 403)
|
||||
|
||||
try:
|
||||
data = json.loads(request.data)
|
||||
email = data["email"].lower()
|
||||
user = app.db.users.find_one({ "email": email }, { "_id": 0 })
|
||||
|
||||
if user and pbkdf2_sha256.verify(data["password"], user["password"]):
|
||||
access_token = auth.encodeAccessToken(user["id"], user["email"], user["plan"])
|
||||
refresh_token = auth.encodeRefreshToken(user["id"], user["email"], user["plan"])
|
||||
|
||||
app.db.users.update({ "id": user["id"] }, { "$set": {
|
||||
"refresh_token": refresh_token,
|
||||
"last_login": tools.nowEpoch()
|
||||
} })
|
||||
|
||||
resp = tools.JsonResp({
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"first_name": user["first_name"],
|
||||
"last_name": user["last_name"],
|
||||
"plan": user["plan"],
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
}, 200)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return resp
|
||||
|
||||
def logout(self):
|
||||
try:
|
||||
tokenData = jwt.decode(request.headers.get("AccessToken"), app.config["SECRET_KEY"])
|
||||
app.db.users.update({ "id": tokenData["user_id"] }, { '$unset': { "refresh_token": "" } })
|
||||
# Note: At some point I need to implement Token Revoking/Blacklisting
|
||||
# General info here: https://flask-jwt-extended.readthedocs.io/en/latest/blacklist_and_token_revoking.html
|
||||
except:
|
||||
pass
|
||||
|
||||
resp = tools.JsonResp({ "message": "User logged out" }, 200)
|
||||
|
||||
return resp
|
||||
|
||||
def add(self):
|
||||
data = json.loads(request.data)
|
||||
|
||||
expected_data = {
|
||||
"first_name": data['first_name'],
|
||||
"last_name": data['last_name'],
|
||||
"email": data['email'].lower(),
|
||||
"password": data['password']
|
||||
}
|
||||
|
||||
# Merge the posted data with the default user attributes
|
||||
self.defaults.update(expected_data)
|
||||
user = self.defaults
|
||||
|
||||
# Encrypt the password
|
||||
user["password"] = pbkdf2_sha256.encrypt(user["password"], rounds=20000, salt_size=16)
|
||||
|
||||
# Make sure there isn"t already a user with this email address
|
||||
existing_email = app.db.users.find_one({ "email": user["email"] })
|
||||
|
||||
if existing_email:
|
||||
resp = tools.JsonResp({
|
||||
"message": "There's already an account with this email address",
|
||||
"error": "email_exists"
|
||||
}, 400)
|
||||
|
||||
else:
|
||||
if app.db.users.save(user):
|
||||
|
||||
# Log the user in (create and return tokens)
|
||||
access_token = auth.encodeAccessToken(user["id"], user["email"], user["plan"])
|
||||
refresh_token = auth.encodeRefreshToken(user["id"], user["email"], user["plan"])
|
||||
|
||||
app.db.users.update({ "id": user["id"] }, {
|
||||
"$set": {
|
||||
"refresh_token": refresh_token
|
||||
}
|
||||
})
|
||||
|
||||
resp = tools.JsonResp({
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"first_name": user["first_name"],
|
||||
"last_name": user["last_name"],
|
||||
"plan": user["plan"],
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
}, 200)
|
||||
|
||||
else:
|
||||
resp = tools.JsonResp({ "message": "User could not be added" }, 400)
|
||||
|
||||
return resp
|
||||
27
api/main/user/routes.py
Normal file
27
api/main/user/routes.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from flask import Blueprint
|
||||
from flask import current_app as app
|
||||
from main.auth import token_required
|
||||
from main.user.models import User
|
||||
|
||||
user_blueprint = Blueprint("user", __name__)
|
||||
|
||||
@user_blueprint.route("/", methods=["GET"])
|
||||
@token_required
|
||||
def get():
|
||||
return User().get()
|
||||
|
||||
@user_blueprint.route("/auth/", methods=["GET"])
|
||||
def getAuth():
|
||||
return User().getAuth()
|
||||
|
||||
@user_blueprint.route("/login/", methods=["POST"])
|
||||
def login():
|
||||
return User().login()
|
||||
|
||||
@user_blueprint.route("/logout/", methods=["GET"])
|
||||
def logout():
|
||||
return User().logout()
|
||||
|
||||
@user_blueprint.route("/", methods=["POST"])
|
||||
def add():
|
||||
return User().add()
|
||||
8
api/run.py
Normal file
8
api/run.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from main import create_app
|
||||
import logging
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = create_app()
|
||||
app.run(host=app.config["FLASK_DOMAIN"], port=app.config["FLASK_PORT"])
|
||||
else:
|
||||
logging.basicConfig(app.config["FLASK_DIRECTORY"] + "trace.log", level=logging.DEBUG)
|
||||
0
api/trace.log
Executable file
0
api/trace.log
Executable file
104
setup.sh
Executable file
104
setup.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/bin/sh
|
||||
|
||||
WHITE='\033[1;37m'
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BROWN='\033[1;33m'
|
||||
BLUE='\033[1;34m'
|
||||
PURPLE='\033[1;35m'
|
||||
CYAN='\033[0;36m'
|
||||
GREY='\033[1;30m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "${GREY}"
|
||||
|
||||
cat << "EOF"
|
||||
####### _____ __
|
||||
###### / ___/___ / /___ ______
|
||||
##### \__ \/ _ \/ __/ / / / __ \
|
||||
#### ___/ / __/ /_/ /_/ / /_/ /
|
||||
### /____/\___/\__/\__,_/ .___/
|
||||
## /_/
|
||||
#
|
||||
|
||||
EOF
|
||||
|
||||
# PIPENV SETUP
|
||||
|
||||
echo "${BLUE}VIRTUAL ENVIRONMENT SETUP${NC}"
|
||||
echo
|
||||
pipenv install
|
||||
echo
|
||||
|
||||
# FLASK SETUP
|
||||
|
||||
echo "${BLUE}FLASK CONFIGURATION${NC}"
|
||||
echo
|
||||
|
||||
# Defaults
|
||||
SECRET_KEY=`cat /dev/urandom | head -c 24 | base64`
|
||||
FLASK_DOMAIN="localhost"
|
||||
FLASK_PORT_DEFAULT=5000
|
||||
FLASK_DIRECTORY="$(pwd)/api/"
|
||||
MONGO_HOSTNAME_DEFAULT="localhost"
|
||||
|
||||
# Domain the Flask app will be running on
|
||||
read -p "Flask App Domain [$FLASK_DOMAIN]: " FLASK_DOMAIN
|
||||
FLASK_DOMAIN=${FLASK_DOMAIN:-$FLASK_DOMAIN}
|
||||
echo
|
||||
|
||||
# Port the Flask app will be running on
|
||||
read -p "Flask App Port [$FLASK_PORT_DEFAULT]: " FLASK_PORT
|
||||
FLASK_PORT=${FLASK_PORT:-$FLASK_PORT_DEFAULT}
|
||||
echo
|
||||
|
||||
# URL of the front-end JavaScript application
|
||||
read -p "Front-End Base URL, port included (e.g. http://localhost, http://localhost:3000): " FRONTEND_DOMAIN
|
||||
echo
|
||||
|
||||
# MongoDB hostname
|
||||
read -p "Mongo Hostname [$MONGO_HOSTNAME_DEFAULT]: " MONGO_HOSTNAME
|
||||
MONGO_HOSTNAME=${MONGO_HOSTNAME:-$MONGO_HOSTNAME_DEFAULT}
|
||||
echo
|
||||
|
||||
# MongoDB database name for the app
|
||||
while [[ $MONGO_APP_DATABASE == '' ]]
|
||||
do
|
||||
read -p "Mongo App Database Name: " MONGO_APP_DATABASE
|
||||
|
||||
if [[ $MONGO_APP_DATABASE == '' ]]
|
||||
then
|
||||
echo "${RED}Required${NC}"
|
||||
fi
|
||||
|
||||
echo
|
||||
done
|
||||
|
||||
# Rename config.cfg.sample to config.cfg
|
||||
CONFIG_EXAMPLE_FILE=./api/main/config/config.cfg.sample
|
||||
CONFIG_FILE=./api/main/config/config.cfg
|
||||
mv $CONFIG_EXAMPLE_FILE $CONFIG_FILE
|
||||
|
||||
# Save configuration values to config.cfg
|
||||
sed -i '' -e "s~##SECRET_KEY##~$SECRET_KEY~g" $CONFIG_FILE
|
||||
sed -i '' -e "s~##FLASK_DOMAIN##~$FLASK_DOMAIN~g" $CONFIG_FILE
|
||||
sed -i '' -e "s~##FLASK_PORT##~$FLASK_PORT~g" $CONFIG_FILE
|
||||
sed -i '' -e "s~##FRONTEND_DOMAIN##~$FRONTEND_DOMAIN~g" $CONFIG_FILE
|
||||
sed -i '' -e "s~##FLASK_DIRECTORY##~$FLASK_DIRECTORY~g" $CONFIG_FILE
|
||||
sed -i '' -e "s~##MONGO_HOSTNAME##~$MONGO_HOSTNAME~g" $CONFIG_FILE
|
||||
sed -i '' -e "s~##MONGO_APP_DATABASE##~$MONGO_APP_DATABASE~g" $CONFIG_FILE
|
||||
|
||||
echo "${GREEN}Flask configuration saved!${NC}"
|
||||
echo
|
||||
|
||||
# INSTRUCTIONS TO START THE APP
|
||||
|
||||
echo "${BLUE}FIRE IT UP!${NC}"
|
||||
echo
|
||||
|
||||
echo "To start the Flask app run these two commands:"
|
||||
echo
|
||||
echo "${GREY}> ${NC}pipenv shell"
|
||||
echo "${GREY}> ${NC}./run.sh"
|
||||
echo
|
||||
90
web/app.css
Normal file
90
web/app.css
Normal file
@@ -0,0 +1,90 @@
|
||||
* {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #2C3A47;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p {
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
margin: 0 auto;
|
||||
padding: 60px 0 120px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.app-panel {
|
||||
margin: 0 0 60px;
|
||||
padding: 26px 30px 32px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
color: #777777;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
margin: 0 0 24px;
|
||||
padding: 7px 12px 7px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
transition: border-color .2s ease;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: #2C3A47;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px 11px;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background: #2bcbba;
|
||||
transition: background-color .2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #10ddc9;
|
||||
}
|
||||
|
||||
.btn[type="submit"] {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
padding: 12px 30px 11px;
|
||||
text-align: center;
|
||||
color: #044e48;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
background: #e0f8f6;
|
||||
}
|
||||
71
web/app.js
Normal file
71
web/app.js
Normal file
@@ -0,0 +1,71 @@
|
||||
$(function() {
|
||||
|
||||
// Add User Submission
|
||||
var $addUserForm = $("#add-user-form"),
|
||||
$addUserSuccess = $("#add-user-success");
|
||||
|
||||
$addUserForm.on("submit", function(e) {
|
||||
|
||||
var data = {
|
||||
first_name: $addUserForm.find("#first_name").val(),
|
||||
last_name: $addUserForm.find("#last_name").val(),
|
||||
email: $addUserForm.find("#email").val(),
|
||||
password: $addUserForm.find("#password").val(),
|
||||
};
|
||||
|
||||
console.log(data);
|
||||
|
||||
$.ajax({
|
||||
url: "http://localhost:5000/user/",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
success: function(resp) {
|
||||
console.log(resp);
|
||||
$addUserForm.hide();
|
||||
$addUserSuccess.show();
|
||||
},
|
||||
error: function(error) {
|
||||
console.error(error);
|
||||
alert(error.responseJSON.message)
|
||||
}
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// User Login Submission
|
||||
var $loginForm = $("#login-form"),
|
||||
$loginSuccess = $("#login-success");
|
||||
|
||||
$loginForm.on("submit", function(e) {
|
||||
|
||||
var data = {
|
||||
email: $loginForm.find("#email").val(),
|
||||
password: $loginForm.find("#password").val(),
|
||||
};
|
||||
|
||||
console.log(data);
|
||||
|
||||
$.ajax({
|
||||
url: "http://localhost:5000/user/login/",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
success: function(resp) {
|
||||
console.log(resp);
|
||||
$loginForm.hide();
|
||||
$loginSuccess.show();
|
||||
},
|
||||
error: function(error) {
|
||||
console.error(error);
|
||||
alert(error.responseJSON.message)
|
||||
}
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
});
|
||||
76
web/index.html
Normal file
76
web/index.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Front-End App</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="app-wrapper">
|
||||
|
||||
<!-- Add User Form -->
|
||||
<div class="app-panel">
|
||||
|
||||
<h3 class="centered">Add User</h3>
|
||||
|
||||
<form id="add-user-form">
|
||||
|
||||
<label for="first_name" class="input-label">First Name</label>
|
||||
<input type="text" name="first_name" id="first_name" class="input-field" required>
|
||||
|
||||
<label for="last_name" class="input-label">Last Name</label>
|
||||
<input type="text" name="last_name" id="last_name" class="input-field" required>
|
||||
|
||||
<label for="email" class="input-label">Email Address</label>
|
||||
<input type="email" name="email" id="email" class="input-field" required>
|
||||
|
||||
<label for="password" class="input-label">Password</label>
|
||||
<input type="password" name="password" id="password" class="input-field" required>
|
||||
|
||||
<div class="centered">
|
||||
<input type="submit" value="Submit" class="btn">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div id="add-user-success" class="success-message">
|
||||
User successfully added!
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Add User Form -->
|
||||
<div class="app-panel">
|
||||
|
||||
<h3 class="centered">Add User</h3>
|
||||
|
||||
<form id="login-form">
|
||||
|
||||
<label for="email" class="input-label">Email Address</label>
|
||||
<input type="email" name="email" id="email" class="input-field" required>
|
||||
|
||||
<label for="password" class="input-label">Password</label>
|
||||
<input type="password" name="password" id="password" class="input-field" required>
|
||||
|
||||
<div class="centered">
|
||||
<input type="submit" value="Submit" class="btn">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div id="login-success" class="success-message">
|
||||
User successfully authenticated!
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="jquery.js"></script>
|
||||
<script src="app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2
web/jquery.js
vendored
Normal file
2
web/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user