diff --git a/.env.example b/.env.example index 2d58f44..0895b6f 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ -DATABASE_URL="mongodb://:@:/ir-commands" \ No newline at end of file +DATABASE_URL="mongodb://:@:/ir-commands" +ADMIN_API_KEY="some admin key" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index cc67606..9fb9a8f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "python.linting.pylintEnabled": true, - "python.linting.enabled": true + "python.linting.enabled": true, + "cSpell.words": [ + "jsonify" + ] } \ No newline at end of file diff --git a/README.md b/README.md index fd20387..5a67c9f 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,13 @@ Simple, light-weight server for RF commands (such as: IR, 433 MHz etc.) for appl # running server -* in project directory press `pip install -r requirements.txt` -* create MongoDB database named `ir-commands`. -* create collection named `commands`. -* set `DATABASE_URL` environment variable the MongoDB URL, -* run it using `python app.py`. -* in production run is recomended using `gunicorn`. +* To clean current dependencies in the machine uninstall all current system packages `pip uninstall -r requirements.txt -y && pip freeze > requirements.txt` +* In project directory press `pip install -r requirements.txt` +* Create MongoDB database named `ir-commands`. +* Create collection named `commands`. +* Set `DATABASE_URL` environment variable the MongoDB URL, +* Run it using `python app.py`. +* In production run is recomended using `gunicorn`. # technologies The Server is Build with Python, with the Flask framework for the HTTP Routing, MongoDB for the data storing. diff --git a/app.py b/app.py index 6bae031..2250b8e 100644 --- a/app.py +++ b/app.py @@ -1,33 +1,10 @@ -from flask import Flask, jsonify +from flask import Flask, jsonify, request, abort import os import settings -from data import get_devices, get_device_commands +import route.rf_route +import route.device_route +from route.rest_common import app -app = Flask(__name__) - -@app.after_request -def remove_header(response): - # Hide server info from possilbe attakers. - response.headers['Server'] = '' - return response - -# Get all supported devices in system -@app.route('/devices') -def models_list(): - return jsonify(get_devices()) - -# Get RF commands of a devices. -@app.route('/rf//') -def model_rf_commands(brand, model): - return jsonify(get_device_commands(brand=brand, model=model)) - - -# Get security help info -@app.route('/.well-known/security.txt') -def security_info(): - return app.send_static_file('.well-known/security.txt') - - # Get PORT from env. PORT = os.getenv("PORT") if not PORT: diff --git a/data.py b/data.py deleted file mode 100644 index 1bedf17..0000000 --- a/data.py +++ /dev/null @@ -1,32 +0,0 @@ -from mongoengine import * -import os - -DATABASE_URL = os.getenv("DATABASE_URL") - -# Connect mongoengine driver to the database -connect(host=DATABASE_URL) - -# Device document model. -class RfDevice(Document): - meta = { - 'collection': 'commands' - } - brand = StringField(required=True) - model = StringField(required=True) - category = StringField(required=True) - commands = DynamicField(required=True) - -def get_devices(): - devices = [] - for device in RfDevice.objects: - record = { - 'brand': device.brand, - 'model': device.model, - 'category': device.category, - } - devices.append(record) - return devices - -def get_device_commands(brand, model): - device = RfDevice.objects.get(brand=brand,model=model) - return device.commands \ No newline at end of file diff --git a/data/devices.py b/data/devices.py new file mode 100644 index 0000000..8fe9e16 --- /dev/null +++ b/data/devices.py @@ -0,0 +1,47 @@ +from data.models import RfDevice + + +def is_device_exists(brand: str, model: str) -> bool: + try: + RfDevice.objects.get(brand=brand, model=model) + return True + except: + return False + + +def get_devices() -> list: + devices = [] + for device in RfDevice.objects: + record = { + 'brand': device.brand, + 'model': device.model, + 'category': device.category, + } + devices.append(record) + return devices + + +def create_device(device) -> None: + brand = device['brand'] + model = device['model'] + if is_device_exists(brand=brand, model=model): + raise Exception("Sorry, the device already exists") + + newDevice = RfDevice(brand=brand, model=model, + category=device['category'], commands=device['commands']) + newDevice.save() + + +def edit_device(brand: str, model: str, device: dict) -> None: + if not is_device_exists(brand=brand, model=model): + raise Exception("Sorry, the device is not exists") + device = RfDevice.objects.get(brand=brand, model=model) + device['brand'] = newName['brand'] + device['model'] = newName['model'] + device['category'] = newName['category'] + device.save() + + +def delete_device(brand: str, model: str) -> None: + device = RfDevice.objects.get(brand=brand, model=model) + device.delete() diff --git a/data/models.py b/data/models.py new file mode 100644 index 0000000..a3d3c2e --- /dev/null +++ b/data/models.py @@ -0,0 +1,18 @@ +from mongoengine import * +import os + +DATABASE_URL = os.getenv("DATABASE_URL") + +# Connect mongoengine driver to the database +connect(host=DATABASE_URL) + + +class RfDevice(Document): + """Devices document objects model.""" + meta = { + 'collection': 'commands' + } + brand = StringField(required=True) + model = StringField(required=True) + category = StringField(required=True) + commands = DynamicField(required=True) diff --git a/data/rf.py b/data/rf.py new file mode 100644 index 0000000..3d14adf --- /dev/null +++ b/data/rf.py @@ -0,0 +1,13 @@ + +from data.models import RfDevice + + +def get_device_commands(brand: str, model: str) -> dict: + device = RfDevice.objects.get(brand=brand, model=model) + return device.commands + + +def set_device_commands(brand: str, model: str, data: dict) -> None: + device = RfDevice.objects.get(brand=brand, model=model) + device.commands = data['commands'] + device.save() diff --git a/requirements.txt b/requirements.txt index 4aa931b..6b697c8 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/route/device_route.py b/route/device_route.py new file mode 100644 index 0000000..5f2e1e6 --- /dev/null +++ b/route/device_route.py @@ -0,0 +1,35 @@ +from flask import Flask, jsonify, request, abort +from route.rest_common import admin_scope, commands_schema, device_schema, editDevice_schema, app +from flask_expects_json import expects_json +from data.devices import edit_device, create_device, delete_device, get_devices + + +@app.route('/devices', methods=['GET']) +def models_list_route(): + return jsonify(get_devices()) + + +@app.route('/device', methods=['POST']) +@expects_json(device_schema) +@admin_scope() +def create_device_route(): + create_device(request.json) + resp = jsonify(success=True) + return resp + + +@app.route('/device//', methods=['PUT']) +@admin_scope() +@expects_json(editDevice_schema) +def set_device_route(brand: str, model: str): + edit_device(brand=brand, model=model, newDevice=request.json) + resp = jsonify(success=True) + return resp + + +@app.route('/device//', methods=['DELETE']) +@admin_scope() +def delete_device_route(brand: str, model: str): + delete_device(brand=brand, model=model) + resp = jsonify(success=True) + return resp diff --git a/route/rest_common.py b/route/rest_common.py new file mode 100644 index 0000000..7de4093 --- /dev/null +++ b/route/rest_common.py @@ -0,0 +1,70 @@ +from flask import Flask, jsonify, request, abort +from functools import wraps +from settings import app_name +import os +import sys + +app = Flask(app_name) + +ADMIN_API_KEY = os.getenv("ADMIN_API_KEY") +if not ADMIN_API_KEY: + msg = 'ADMIN_API_KEY not exists... exiting' + print(msg) + sys.exit(msg) + +device_schema = { + "type": "object", + "properties": { + "brand": {"type": "string"}, + "model": {"type": "string"}, + "category": {"type": "string"}, + "commands": {"type": "object"}, + }, + 'required': ['brand', 'model', 'category', 'commands'] +} + +commands_schema = { + "type": "object", + "properties": { + "commands": {"type": "object"}, + }, + 'required': ['commands'] +} + +editDevice_schema = { + "type": "object", + "properties": { + "brand": {"type": "string"}, + "model": {"type": "string"}, + "category": {"type": "string"}, + }, + 'required': ['brand', 'model', 'category'] +} + + +@app.after_request +def remove_header(response): + # Hide server info from possilbe attakers. + response.headers['Server'] = '' + return response + + +def admin_scope(): + """Very simple admin scope authorazed""" + def _admin_scope(f): + @wraps(f) + def __admin_scope(*args, **kwargs): + try: + if request.headers['api-key'] != ADMIN_API_KEY: + raise Exception('Invalid api-key') + except: + abort(403, 'Invalid api-key') + return f(*args, **kwargs) + return __admin_scope + return _admin_scope + + +@app.route('/.well-known/security.txt') +def security_info_route(): + """ Get security help info """ + return app.send_static_file('.well-known/security.txt') diff --git a/route/rf_route.py b/route/rf_route.py new file mode 100644 index 0000000..84d19c1 --- /dev/null +++ b/route/rf_route.py @@ -0,0 +1,18 @@ +from flask import Flask, jsonify, request, abort +from route.rest_common import app, admin_scope, commands_schema +from flask_expects_json import expects_json +from data.rf import set_device_commands, get_device_commands + + +@app.route('/rf//', methods=['GET']) +def model_rf_commands_route(brand: str, model: str): + return jsonify(get_device_commands(brand=brand, model=model)) + + +@app.route('/rf//', methods=['PUT']) +@admin_scope() +@expects_json(commands_schema) +def set_device_commans_route(brand: str, model: str): + set_device_commands(brand=brand, model=model, data=request.json) + resp = jsonify(success=True) + return resp diff --git a/runtime.txt b/runtime.txt index 6f651a3..385705b 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.3 +python-3.8.3 diff --git a/settings.py b/settings.py index 4c4f118..3d72ca7 100644 --- a/settings.py +++ b/settings.py @@ -1,3 +1,5 @@ from dotenv import load_dotenv import os load_dotenv() + +app_name = 'rf-commands-repo'