initial structure

dev
Anton Franzluebbers 2021-10-15 21:39:52 -04:00
commit ca462d6cd8
17 changed files with 509 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
env/
app.log
__pycache__/

34
CreateDB.sql Normal file
View File

@ -0,0 +1,34 @@
DROP TABLE IF EXISTS `Room`;
CREATE TABLE `Room` (
`room_id` VARCHAR(64) NOT NULL PRIMARY KEY,
`date_created` TIMESTAMP DEFAULT CURRENT_TIME,
`last_modified` TIMESTAMP DEFAULT CURRENT_TIME,
-- Can be null if no owner
`owner` VARCHAR(64),
-- array of hw_ids of users allowed. Always includes the owner. Null for public
`whitelist` JSON,
CHECK (JSON_VALID(`whitelist`))
);
DROP TABLE IF EXISTS `Headset`;
CREATE TABLE `Headset` (
`hw_id` VARCHAR(64) NOT NULL PRIMARY KEY,
-- The room_id of the owned room
`owned_room` VARCHAR(64),
-- The room_id of the current room. Can be null if room not specified
`current_room` VARCHAR(64),
-- changes relatively often
`pairing_code` INT,
`date_created` TIMESTAMP DEFAULT CURRENT_TIME,
-- the last time this headset was actually seen
`last_used` TIMESTAMP DEFAULT CURRENT_TIME
);
DROP TABLE IF EXISTS `APIKey`;
CREATE TABLE `APIKey` (
`key` VARCHAR(64) NOT NULL PRIMARY KEY,
-- 0 is all access, higher is less
-- 10 is for headset clients
`auth_level` INT,
`date_created` TIMESTAMP DEFAULT CURRENT_TIME,
`last_used` TIMESTAMP DEFAULT CURRENT_TIME,
`uses` INT DEFAULT 0
);

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
flask
simplejson
Flask-Limiter
pymysql

82
velconnect/__init__.py Normal file
View File

@ -0,0 +1,82 @@
from flask import Flask, jsonify, make_response, request
from velconnect.auth import limiter
from velconnect.logger import logger
from time import strftime
import traceback
def create_app():
app = Flask(
__name__,
instance_relative_config=False,
)
app.config.from_pyfile('config.py')
limiter.init_app(app)
from .routes import api
app.register_blueprint(api.bp, url_prefix='/api')
from .routes import website
app.register_blueprint(website.bp)
# Error handlers
app.register_error_handler(404, resource_not_found)
app.register_error_handler(401, noapikey_handler)
app.register_error_handler(429, ratelimit_handler)
app.register_error_handler(Exception, exceptions)
app.after_request(after_request)
return app
# @app.after_request
def after_request(response):
""" Logging after every request. """
# This avoids the duplication of registry in the log,
# since that 500 is already logged via @app.errorhandler.
if response.status_code != 500:
ts = strftime('[%Y-%b-%d %H:%M]')
logger.error('%s %s %s %s %s %s',
ts,
request.remote_addr,
request.method,
request.scheme,
request.full_path,
response.status)
return response
# @app.errorhandler(Exception)
def exceptions(e):
""" Logging after every Exception. """
ts = strftime('[%Y-%b-%d %H:%M]')
tb = traceback.format_exc()
logger.error('%s %s %s %s %s 5xx INTERNAL SERVER ERROR\n%s',
ts,
request.remote_addr,
request.method,
request.scheme,
request.full_path,
tb)
return "SERVER ERROR", 500
# @app.errorhandler(429)
def ratelimit_handler(e):
return make_response(
jsonify(error="ratelimit exceeded %s" % e.description), 429
)
def resource_not_found(e):
return jsonify(error=str(e)), 404
# @app.errorhandler(401)
def noapikey_handler(e):
return make_response(
jsonify(error="not authorized %s" % e.description), 401
)

56
velconnect/auth.py Normal file
View File

@ -0,0 +1,56 @@
from functools import wraps
import inspect
from flask import abort, request, make_response, jsonify
from velconnect.db import connectToDB
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
def require_api_key(level):
def decorator(view_function):
def wrapper(*args, **kwargs):
key = request.headers.get('x-api-key')
conn, curr = connectToDB(request)
query = """
SELECT * FROM `APIKey` WHERE `key`=%(key)s;
"""
curr.execute(query, {'key': key})
values = [dict(row) for row in curr.fetchall()]
if len(values) > 0 and values['auth_level'] < level:
return view_function(*args, **kwargs)
else:
abort(401)
wrapper.__name__ = view_function.__name__
return wrapper
return decorator
def required_args(f):
@wraps(f)
def decorated_function(*args, **kwargs):
""" Decorator that makes sure the view arguments are in the request args, otherwise 400 error """
sig = inspect.signature(f)
data = request.args
for arg in sig.parameters.values():
# Check if the argument is passed from the url
if arg.name in kwargs:
continue
# check if the argument is in the json data
if data and data.get(arg.name) is not None:
kwargs[arg.name] = data.get(arg.name)
# else check if it has been given a default
elif arg.default is not arg.empty:
kwargs[arg.name] = arg.default
missing_args = [arg for arg in sig.parameters.keys()
if arg not in kwargs.keys()]
if missing_args:
return 'Did not receive args for: {}'.format(', '.join(missing_args)), 400
return f(*args, **kwargs)
return decorated_function

3
velconnect/config.py Normal file
View File

@ -0,0 +1,3 @@
# Flask-Caching related configs
CACHE_TYPE = "SimpleCache"
CACHE_DEFAULT_TIMEOUT = 3600

View File

@ -0,0 +1,4 @@
MYSQL_DATABASE_USER = 'root'
MYSQL_DATABASE_PASSWORD = 'w2e3t5t5'
MYSQL_DATABASE_HOST = 'localhost'
MYSQL_DATABASE_DB = 'vel_headset_pairing'

20
velconnect/db.py Normal file
View File

@ -0,0 +1,20 @@
# from config import *
import pymysql
from pymysql import converters
from velconnect.config_mysql import *
def connectToDB():
conv = converters.conversions.copy()
conv[246] = float # convert decimals to floats
conn = pymysql.connect(
host=MYSQL_DATABASE_HOST,
user=MYSQL_DATABASE_USER,
password=MYSQL_DATABASE_PASSWORD,
db=MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor,
conv=conv
)
curr = conn.cursor()
return conn, curr

7
velconnect/logger.py Normal file
View File

@ -0,0 +1,7 @@
import logging
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler('app.log', maxBytes=100000000, backupCount=1500)
logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)
logger.addHandler(handler)

110
velconnect/routes/api.py Normal file
View File

@ -0,0 +1,110 @@
from velconnect.auth import require_api_key
from velconnect.db import connectToDB
from velconnect.logger import logger
from flask import Blueprint, request, jsonify
import time
import simplejson as json
from random import random
bp = Blueprint('api', __name__)
@bp.route('/get_all_headsets', methods=['GET'])
@require_api_key(0)
def get_all_headsets():
conn, curr = connectToDB()
query = """
SELECT * FROM `Headset`;
"""
curr.execute(query, None)
values = [dict(row) for row in curr.fetchall()]
curr.close()
return jsonify(values)
@bp.route('/pair_headset/<pairing_code>', methods=['POST'])
@require_api_key(0)
def pair_headset(pairing_code):
conn, curr = connectToDB()
query = """
SELECT * FROM `Headset` WHERE `pairing_code`=%(pairing_code)s;
"""
curr.execute(query, {'pairing_code': pairing_code})
values = [dict(row) for row in curr.fetchall()]
curr.close()
if len(values) > 0:
return jsonify({'hw_id': values['hw_id']})
return 'Not found', 400
# This also creates a headset if it doesn't exist
@bp.route('/update_pairing_code', methods=['POST'])
@require_api_key(0)
def update_paring_code():
data = request.json
if 'hw_id' not in data:
return 'Must supply hw_id', 400
if 'pairing_code' not in data:
return 'Must supply pairing_code', 400
if 'pairing_code' not in data:
return 'Must supply pairing_code', 400
conn, curr = connectToDB()
query = """
INSERT INTO `Headset`(
`hw_id`,
`pairing_code`,
`last_used`
) VALUES (
%(hw_id)s,
%(pairing_code)s,
CURRENT_TIMESTAMP
)
ON DUPLICATE UPDATE
pairing_code=%(pairing_code)s,
last_used=CURRENT_TIMESTAMP;
"""
curr.execute(query, data)
conn.commit()
curr.close()
return 'Success'
@bp.route('/get_room_details/<room_id>', methods=['GET'])
@require_api_key(10)
def get_room_details(room_id):
return jsonify(get_room_details_db(room_id))
def get_room_details_db(room_id):
conn, curr = connectToDB()
query = """
SELECT * FROM `Room` WHERE room_id=%(room_id)s;
"""
curr.execute(query, {'room_id': room_id})
values = [dict(row) for row in curr.fetchall()]
curr.close()
return jsonify(values)
@bp.route('/create_room', methods=['GET'])
@require_api_key(10)
def create_room():
return jsonify(create_room_db())
def create_room_db():
room_id = random.randint(0, 9999)
conn, curr = connectToDB()
query = """
INSERT INTO `Room` VALUES(
%(room_id)s
);
"""
curr.execute(query, {'room_id': room_id})
conn.commit()
curr.close()
return jsonify({'room_id': room_id})

View File

@ -0,0 +1,27 @@
from velconnect.db import connectToDB
from flask import Blueprint, request, jsonify, render_template
from velconnect.logger import logger
import time
import simplejson as json
bp = Blueprint('website', __name__, template_folder='templates')
@bp.route('/', methods=['GET'])
def index():
return render_template('index.jinja')
@bp.route('/pair', methods=['GET'])
def pair():
return render_template('pair.jinja')
@bp.route('/success', methods=['GET'])
def success():
return render_template('success.jinja')
@bp.route('/failure', methods=['GET'])
def failure():
return render_template('failure.jinja')

1
velconnect/static/css/spectre.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,25 @@
<html>
<head>
<style>
body {
background-color: #333;
font-size: 1.2em;
}
.centered {
margin: 8em auto;
width: max-content;
font-family: arial, sans-serif;
color: #ddd;
}
</style>
</head>
<body>
<div class="centered">
🤮 FAIL 🤡
</div>
</body>
</html>

View File

@ -0,0 +1,25 @@
<html>
<head>
<style>
body {
background-color: #333;
font-size: 1.2em;
}
.centered {
margin: 8em auto;
width: max-content;
font-family: arial, sans-serif;
color: #ddd;
}
</style>
</head>
<body>
<div class="centered">
Headset pairing. Wow so cool! 😋😎
</div>
</body>
</html>

View File

@ -0,0 +1,83 @@
<html>
<head>
<link rel="stylesheet" href="/static/css/spectre.min.css">
<style>
#pair_code {
max-width: 4em;
}
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
.centered {
margin: auto;
}
</style>
</head>
<body>
<div class="container">
<!-- <div class="hero bg-gray">
<div class="hero-body">
<h1>Pair your headset.</h1>
</div>
</div> -->
<div class="card">
<div class="card-image">
<img src="/static/img/pair_code_screenshot.png" class="img-responsive">
</div>
<div class="card-header">
<div class="card-title h5">Enter Pairing Code</div>
<div class="card-subtitle text-gray"></div>
</div>
<div class="card-body">
You can find the code in the bottom left of your menu tablet in conVRged.
</div>
<div class="card-footer centered">
<input class="btn" type="text" id="pair_code" placeholder="0000">
<button class="btn btn-primary" id="submit_pairing_code">Submit</button>
</div>
</div>
</div>
<script>
function httpGetAsync(theUrl, callback, failCallback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
callback(xmlHttp.responseText);
}
else {
failCallback(xmlHttp.status);
}
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
}
let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code');
submit_button.addEventListener('click', () => {
httpGetAsync('/api/pair_headset/' + pair_code_input.value, (resp) => {
console.log(resp);
let respData = JSON.parse(resp);
if (respData['hw_id'] != '') {
document.cookie = "hw_id="+respData['hw_id'];
window.location.href = "/success";
}
}, (status) => {
window.location.href = "/failure";
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,25 @@
<html>
<head>
<style>
body {
background-color: #333;
font-size: 1.2em;
}
.centered {
margin: 8em auto;
width: max-content;
font-family: arial, sans-serif;
color: #ddd;
}
</style>
</head>
<body>
<div class="centered">
🎉 SUCCESS 🎉
</div>
</body>
</html>