initial structure
commit
ca462d6cd8
|
|
@ -0,0 +1,3 @@
|
|||
env/
|
||||
app.log
|
||||
__pycache__/
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
flask
|
||||
simplejson
|
||||
Flask-Limiter
|
||||
pymysql
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Flask-Caching related configs
|
||||
CACHE_TYPE = "SimpleCache"
|
||||
CACHE_DEFAULT_TIMEOUT = 3600
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
MYSQL_DATABASE_USER = 'root'
|
||||
MYSQL_DATABASE_PASSWORD = 'w2e3t5t5'
|
||||
MYSQL_DATABASE_HOST = 'localhost'
|
||||
MYSQL_DATABASE_DB = 'vel_headset_pairing'
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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})
|
||||
|
|
@ -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')
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue