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