diff --git a/README.md b/README.md index 5b04ad2..d283972 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ * hideable sidebar with dragable reveal button * user login system * 'app templates' which are sample config entries for popular self hosted apps -* ability to display rest api data on application cards +* powerful plugin system for adding data from various sources to display on cards +* multiple users, access groups, access settings +* tagging system ## Installation ### Docker @@ -58,15 +60,4 @@ If you change the config.ini file, you either have to restart the container (or * Jinja2 * Materialize css * JavaScript/jQuery/jQueryUI -* Requests (python) - -## Version 1.0 TODO list -- [ ] finish rest api data functions (post requests, auth) -- [ ] finish rest api data display (make it look prettier) -- [ ] include nginx & gunicorn in docker container -- [ ] tag/folder system & support for services without web redirection -- [ ] add more template apps from popular self hosted apps -- [ ] pull request template for adding template apps -- [ ] rest api data examples for template apps -- [ ] find a way to mirror this repo on GitHub for exposure -- [ ] support multiple users \ No newline at end of file +* Requests (python) \ No newline at end of file diff --git a/config_readme.md b/config_readme.md new file mode 100644 index 0000000..8c30c7c --- /dev/null +++ b/config_readme.md @@ -0,0 +1,85 @@ +#### Config.ini Readme + +##### Settings +This is the configuration entry for DashMachine's settings. DashMachine will not work if +this is missing. As for all config entries, [Settings] can only appear once in the config. +If you change the config.ini file, you either have to restart the container +(or python script) or click the ‘save’ button in the config section of settings for the +config to be applied. +```ini +[Settings] +theme = light +accent = orange +background = None +roles = admin,user,public_user +home_access_groups = admin_only +settings_access_groups = admin_only +``` + +| Variable | Required | Description | Options | +|------------------------|----------|----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Settings] | Yes | Config section name. | [Settings] | +| theme | Yes | UI theme. | light, dark | +| accent | Yes | UI accent color | orange, red, pink, purple, deepPurple, indigo, blue, lightBlue,cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey | +| background | Yes | Background image for the UI | /static/images/backgrounds/yourpicture.png, external link to image, None, random | +| roles | No | User roles for access groups. | comma separated string, if not defined, this is set to 'admin,user,public_user'. Note: admin, user, public_user roles are required and will be added automatically if omitted. | +| home_access_groups | No | Define which access groups can access the /home page | Groups defined in your config. If not defined, default is admin_only | +| settings_access_groups | No | Define which access groups can access the /settings page | Groups defined in your config. If not defined, default is admin_only | + +##### Apps +These entries are the cards that you see one the home page, as well as the sidenav. Entries +must be unique. They are displayed in the order that they appear in config.ini +```ini +[App Name] +prefix = https:// +url = your-website.com +icon = static/images/apps/default.png +sidebar_icon = static/images/apps/default.png +description = Example description +open_in = iframe +data_sources = None +tags = Example Tag +groups = admin_only +``` + +| Variable | Required | Description | Options | +|--------------|----------|-------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| [App Name] | Yes | The name of your app. | [App Name] | +| prefix | Yes | The prefix for the app's url. | web prefix, e.g. http:// or https:// | +| url | Yes | The url for your app. | web url, e.g. myapp.com | +| open_in | Yes | open the app in the current tab, an iframe or a new tab | iframe, new_tab, this_tab | +| icon | No | Icon for the dashboard. | /static/images/icons/yourpicture.png, external link to image | +| sidebar_icon | No | Icon for the sidenav. | /static/images/icons/yourpicture.png, external link to image | +| description | No | A short description for the app. | string | +| data_sources | No | Data sources to be included on the app's card.*Note: you must have a data source set up in the config above this application entry. | comma separated string | +| tags | No | Optionally specify tags for organization on /home | comma separated string | +| groups | No | Optionally the access groups that can see this app. | comma separated string | + +##### Access Groups +You can create access groups to control what user roles can access parts of the ui. Each +application can have an access group, if the user's role is not in the group, the app will be hidden. +Also, in the settings entry you can specify `home_access_groups` and `settings_access_groups` to control +which groups can access /home and /settings +```ini +[public] +roles = admin, user, public_user +``` + +> **Note:** if no access groups are defined in the config, the application will create a default group called 'admin_only' with 'roles = admin' + +| Variable | Required | Description | Options | +|--------------|----------|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| [Group Name] | Yes | Name for access group. | [Group Name] | +| roles | Yes | A comma separated list of user roles allowed to view apps in this access group | Roles defined in your config. If not defined, defaults are admin and public_user | + +#### Data Source Platforms +DashMachine includes several different 'platforms' for displaying data on your dash applications. +Platforms are essentially plugins. All data source config entries require the `plaform` variable, +which tells DashMachine which platform file in the platform folder to load. **Note:** you are able to +load your own plaform files by placing them in the platform folder and referencing them in the config. +However currently they will be deleted if you update the application, if you would like to make them +permanent, submit a pull request for it to be added by default! + +> To add a data source to your app, add a data source config entry from one of the samples below +**above** the application entry in config.ini, then add the following to your app config entry: +`data_source = variable_name` \ No newline at end of file diff --git a/dashmachine/__init__.py b/dashmachine/__init__.py index e7e0c4b..1ac0c06 100755 --- a/dashmachine/__init__.py +++ b/dashmachine/__init__.py @@ -6,9 +6,10 @@ from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_login import LoginManager from flask_restful import Api +from dashmachine.paths import user_data_folder -if not os.path.isdir("dashmachine/user_data"): - os.mkdir("dashmachine/user_data") +if not os.path.isdir(user_data_folder): + os.mkdir(user_data_folder) app = Flask(__name__) diff --git a/dashmachine/error_pages/routes.py b/dashmachine/error_pages/routes.py index 10e4eb9..be488bd 100755 --- a/dashmachine/error_pages/routes.py +++ b/dashmachine/error_pages/routes.py @@ -19,3 +19,8 @@ def error_403(error): @error_pages.app_errorhandler(500) def error_500(error): return render_template("/error_pages/500.html"), 500 + + +@error_pages.route("/unauthorized") +def unauthorized(): + return render_template("/error_pages/unauthorized.html") diff --git a/dashmachine/main/forms.py b/dashmachine/main/forms.py index e69de29..21bc2be 100755 --- a/dashmachine/main/forms.py +++ b/dashmachine/main/forms.py @@ -0,0 +1,6 @@ +from flask_wtf import FlaskForm +from wtforms import SelectField + + +class TagsForm(FlaskForm): + tags = SelectField(choices=[("All tags", "All tags")]) diff --git a/dashmachine/main/models.py b/dashmachine/main/models.py index 8a6f749..9637d1e 100644 --- a/dashmachine/main/models.py +++ b/dashmachine/main/models.py @@ -1,5 +1,11 @@ from dashmachine import db +rel_app_data_source = db.Table( + "rel_app_data_source", + db.Column("data_source_id", db.Integer, db.ForeignKey("data_sources.id")), + db.Column("app_id", db.Integer, db.ForeignKey("apps.id")), +) + class Files(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -20,6 +26,8 @@ class Apps(db.Model): description = db.Column(db.String()) open_in = db.Column(db.String()) data_template = db.Column(db.String()) + groups = db.Column(db.String()) + tags = db.Column(db.String()) class TemplateApps(db.Model): @@ -33,17 +41,31 @@ class TemplateApps(db.Model): open_in = db.Column(db.String()) -class ApiCalls(db.Model): +class DataSources(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String()) - resource = db.Column(db.String()) - method = db.Column(db.String()) - payload = db.Column(db.String()) - authentication = db.Column(db.String()) - username = db.Column(db.String()) - password = db.Column(db.String()) - value_template = db.Column(db.String()) + platform = db.Column(db.String()) + args = db.relationship("DataSourcesArgs", backref="data_source") + apps = db.relationship( + "Apps", + secondary=rel_app_data_source, + backref=db.backref("data_sources", lazy="dynamic"), + ) -db.create_all() -db.session.commit() +class DataSourcesArgs(db.Model): + id = db.Column(db.Integer, primary_key=True) + key = db.Column(db.String()) + value = db.Column(db.String()) + data_source_id = db.Column(db.Integer, db.ForeignKey("data_sources.id")) + + +class Groups(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String()) + roles = db.Column(db.String()) + + +class Tags(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String()) diff --git a/dashmachine/main/read_config.py b/dashmachine/main/read_config.py new file mode 100644 index 0000000..040e145 --- /dev/null +++ b/dashmachine/main/read_config.py @@ -0,0 +1,176 @@ +import os +from configparser import ConfigParser +from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs, Tags +from dashmachine.settings_system.models import Settings +from dashmachine.paths import user_data_folder +from dashmachine import db + + +def row2dict(row): + d = {} + for column in row.__table__.columns: + d[column.name] = str(getattr(row, column.name)) + + return d + + +def read_config(): + config = ConfigParser() + try: + config.read(os.path.join(user_data_folder, "config.ini")) + except Exception as e: + return {"msg": f"Invalid Config: {e}."} + + ds_list = DataSources.query.all() + for ds in ds_list: + ds.apps.clear() + DataSources.query.delete() + DataSourcesArgs.query.delete() + Apps.query.delete() + Settings.query.delete() + Groups.query.delete() + Tags.query.delete() + + for section in config.sections(): + + # Settings creation + if section == "Settings": + settings = Settings() + if "theme" in config["Settings"]: + settings.theme = config["Settings"]["theme"] + else: + settings.theme = "light" + + if "accent" in config["Settings"]: + settings.accent = config["Settings"]["accent"] + else: + settings.accent = "orange" + + if "background" in config["Settings"]: + settings.background = config["Settings"]["background"] + else: + settings.background = "None" + + if "roles" in config["Settings"]: + settings.roles = config["Settings"]["roles"] + if "admin" not in settings.roles: + settings.roles += ",admin" + if "user" not in settings.roles: + settings.roles += ",user" + if "public_user" not in settings.roles: + settings.roles += ",public_user" + else: + settings.roles = "admin,user,public_user" + + if "home_access_groups" in config["Settings"]: + settings.home_access_groups = config["Settings"]["home_access_groups"] + else: + settings.home_access_groups = "admin_only" + + if "settings_access_groups" in config["Settings"]: + settings.settings_access_groups = config["Settings"][ + "settings_access_groups" + ] + else: + settings.settings_access_groups = "admin_only" + + db.session.add(settings) + db.session.commit() + + # Groups creation + elif "roles" in config[section]: + group = Groups() + group.name = section + group.roles = config[section]["roles"] + db.session.add(group) + db.session.commit() + + # Data source creation + elif "platform" in config[section]: + data_source = DataSources() + data_source.name = section + data_source.platform = config[section]["platform"] + db.session.add(data_source) + db.session.commit() + for key, value in config[section].items(): + if key not in ["name", "platform"]: + arg = DataSourcesArgs() + arg.key = key + arg.value = value + arg.data_source = data_source + db.session.add(arg) + db.session.commit() + + else: + # App creation + app = Apps() + app.name = section + if "prefix" in config[section]: + app.prefix = config[section]["prefix"] + else: + return {"msg": f"Invalid Config: {section} does not contain prefix."} + + if "url" in config[section]: + app.url = config[section]["url"] + else: + return {"msg": f"Invalid Config: {section} does not contain url."} + + if "icon" in config[section]: + app.icon = config[section]["icon"] + else: + app.icon = None + + if "sidebar_icon" in config[section]: + app.sidebar_icon = config[section]["sidebar_icon"] + else: + app.sidebar_icon = app.icon + + if "description" in config[section]: + app.description = config[section]["description"] + else: + app.description = None + + if "open_in" in config[section]: + app.open_in = config[section]["open_in"] + else: + app.open_in = "this_tab" + + if "groups" in config[section]: + app.groups = config[section]["groups"] + else: + app.groups = None + + if "tags" in config[section]: + app.tags = config[section]["tags"].title() + for tag in app.tags.split(","): + tag = tag.strip().title() + if not Tags.query.filter_by(name=tag).first(): + tag_db = Tags(name=tag) + db.session.add(tag_db) + db.session.commit() + else: + app.tags = None + + db.session.add(app) + db.session.commit() + + if "data_sources" in config[section]: + for config_ds in config[section]["data_sources"].split(","): + db_ds = DataSources.query.filter_by(name=config_ds.strip()).first() + if db_ds: + app.data_sources.append(db_ds) + db.session.merge(app) + db.session.commit() + else: + return { + "msg": f"Invalid Config: {section} has a data_source variable that doesn't exist." + } + + group = Groups.query.filter_by(name="admin_only").first() + if not group: + group = Groups() + group.name = "admin_only" + group.roles = "admin" + db.session.add(group) + db.session.commit() + return {"msg": "success", "settings": row2dict(settings)} diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index 32a3952..fbb4e0f 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -4,8 +4,14 @@ from secrets import token_hex from htmlmin.main import minify from flask import render_template, url_for, redirect, request, Blueprint, jsonify from flask_login import current_user -from dashmachine.main.models import Files -from dashmachine.main.utils import get_rest_data +from dashmachine.main.models import Files, Apps, DataSources, Tags +from dashmachine.main.forms import TagsForm +from dashmachine.main.utils import ( + public_route, + check_groups, + get_data_source, +) +from dashmachine.settings_system.models import Settings from dashmachine.paths import cache_folder from dashmachine import app, db @@ -49,21 +55,35 @@ def check_valid_login(): # ------------------------------------------------------------------------------ # /home # ------------------------------------------------------------------------------ +@public_route @main.route("/") @main.route("/home", methods=["GET", "POST"]) def home(): - return render_template("main/home.html") + tags_form = TagsForm() + tags_form.tags.choices += [ + (tag.name, tag.name) for tag in Tags.query.order_by(Tags.name).all() + ] + settings = Settings.query.first() + if not check_groups(settings.home_access_groups, current_user): + return redirect(url_for("error_pages.unauthorized")) + return render_template("main/home.html", tags_form=tags_form) -@main.route("/app_view?", methods=["GET"]) -def app_view(url): - return render_template("main/app-view.html", url=f"https://{url}") +@public_route +@main.route("/app_view?", methods=["GET"]) +def app_view(app_id): + settings = Settings.query.first() + if not check_groups(settings.home_access_groups, current_user): + return redirect(url_for("user_system.login")) + app_db = Apps.query.filter_by(id=app_id).first() + return render_template("main/app-view.html", url=f"{app_db.prefix}{app_db.url}") -@main.route("/load_rest_data", methods=["GET"]) -def load_rest_data(): - data_template = get_rest_data(request.args.get("template")) - return data_template +@main.route("/load_data_source", methods=["GET"]) +def load_data_source(): + data_source = DataSources.query.filter_by(id=request.args.get("id")).first() + data = get_data_source(data_source) + return data # ------------------------------------------------------------------------------ diff --git a/dashmachine/main/utils.py b/dashmachine/main/utils.py index a144ad5..b4d23ba 100755 --- a/dashmachine/main/utils.py +++ b/dashmachine/main/utils.py @@ -1,10 +1,12 @@ import os import subprocess +import importlib from shutil import copyfile from requests import get from configparser import ConfigParser from dashmachine.paths import dashmachine_folder, images_folder, root_folder -from dashmachine.main.models import Apps, ApiCalls, TemplateApps +from dashmachine.main.models import TemplateApps, Groups +from dashmachine.main.read_config import read_config from dashmachine.settings_system.models import Settings from dashmachine.user_system.models import User from dashmachine.user_system.utils import add_edit_user @@ -19,119 +21,6 @@ def row2dict(row): return d -def read_config(): - config = ConfigParser() - try: - config.read("dashmachine/user_data/config.ini") - except Exception as e: - return {"msg": f"Invalid Config: {e}."} - - Apps.query.delete() - ApiCalls.query.delete() - Settings.query.delete() - - try: - settings = Settings( - theme=config["Settings"]["theme"], - accent=config["Settings"]["accent"], - background=config["Settings"]["background"], - ) - db.session.add(settings) - db.session.commit() - except Exception as e: - return {"msg": f"Invalid Config: {e}."} - - for section in config.sections(): - if section != "Settings": - - # API call creation - if "platform" in config[section]: - api_call = ApiCalls() - api_call.name = section - if "resource" in config[section]: - api_call.resource = config[section]["resource"] - else: - return { - "msg": f"Invalid Config: {section} does not contain resource." - } - - if "method" in config[section]: - api_call.method = config[section]["method"] - else: - api_call.method = "GET" - - if "payload" in config[section]: - api_call.payload = config[section]["payload"] - else: - api_call.payload = None - - if "authentication" in config[section]: - api_call.authentication = config[section]["authentication"] - else: - api_call.authentication = None - - if "username" in config[section]: - api_call.username = config[section]["username"] - else: - api_call.username = None - - if "password" in config[section]: - api_call.password = config[section]["password"] - else: - api_call.password = None - - if "value_template" in config[section]: - api_call.value_template = config[section]["value_template"] - else: - api_call.value_template = section - - db.session.add(api_call) - db.session.commit() - continue - - # App creation - app = Apps() - app.name = section - if "prefix" in config[section]: - app.prefix = config[section]["prefix"] - else: - return {"msg": f"Invalid Config: {section} does not contain prefix."} - - if "url" in config[section]: - app.url = config[section]["url"] - else: - return {"msg": f"Invalid Config: {section} does not contain url."} - - if "icon" in config[section]: - app.icon = config[section]["icon"] - else: - app.icon = None - - if "sidebar_icon" in config[section]: - app.sidebar_icon = config[section]["sidebar_icon"] - else: - app.sidebar_icon = app.icon - - if "description" in config[section]: - app.description = config[section]["description"] - else: - app.description = None - - if "open_in" in config[section]: - app.open_in = config[section]["open_in"] - else: - app.open_in = "this_tab" - - if "data_template" in config[section]: - app.data_template = config[section]["data_template"] - else: - app.data_template = None - - db.session.add(app) - db.session.commit() - return {"msg": "success", "settings": row2dict(settings)} - - def read_template_apps(): config = ConfigParser() try: @@ -165,13 +54,17 @@ def public_route(decorated_function): def dashmachine_init(): + db.create_all() + db.session.commit() + migrate_cmd = "python " + os.path.join(root_folder, "manage_db.py db stamp head") + subprocess.run(migrate_cmd, stderr=subprocess.PIPE, shell=True, encoding="utf-8") + migrate_cmd = "python " + os.path.join(root_folder, "manage_db.py db migrate") subprocess.run(migrate_cmd, stderr=subprocess.PIPE, shell=True, encoding="utf-8") upgrade_cmd = "python " + os.path.join(root_folder, "manage_db.py db upgrade") subprocess.run(upgrade_cmd, stderr=subprocess.PIPE, shell=True, encoding="utf-8") - read_config() read_template_apps() user_data_folder = os.path.join(dashmachine_folder, "user_data") @@ -193,28 +86,54 @@ def dashmachine_init(): config_file = os.path.join(user_data_folder, "config.ini") if not os.path.exists(config_file): copyfile("default_config.ini", config_file) - read_config() + + read_config() user = User.query.first() if not user: - add_edit_user(username="admin", password="adminadmin") + settings = Settings.query.first() + add_edit_user( + username="admin", + password="adminadmin", + role=settings.roles.split(",")[0].strip(), + ) + + users = User.query.all() + for user in users: + if not user.role: + user.role = "admin" -def get_rest_data(template): - while template and template.find("{{") > -1: - start_braces = template.find("{{") + 2 - end_braces = template.find("}}") - key = template[start_braces:end_braces].strip() - key_w_braces = template[start_braces - 2 : end_braces + 2] - value = do_api_call(key) - template = template.replace(key_w_braces, value) - return template +def check_groups(groups, current_user): + if current_user.is_anonymous: + current_user.role = "public_user" + + if groups: + groups_list = groups.split(",") + roles_list = [] + for group in groups_list: + group = Groups.query.filter_by(name=group.strip()).first() + for group_role in group.roles.split(","): + roles_list.append(group_role.strip()) + if current_user.role in roles_list: + return True + else: + return False + else: + if current_user.role == "admin": + return True + else: + return False -def do_api_call(key): - api_call = ApiCalls.query.filter_by(name=key).first() - if api_call.method.upper() == "GET": - value = get(api_call.resource) - exec(f"{key} = {value.json()}") - value = str(eval(api_call.value_template)) - return value +def get_data_source(data_source): + data_source_args = {} + for arg in data_source.args: + arg = row2dict(arg) + data_source_args[arg.get("key")] = arg.get("value") + data_source = row2dict(data_source) + module = importlib.import_module( + f"dashmachine.platform.{data_source['platform']}", "." + ) + platform = module.Platform(data_source, **data_source_args) + return platform.process() diff --git a/dashmachine/paths.py b/dashmachine/paths.py index 7397a4f..800487e 100755 --- a/dashmachine/paths.py +++ b/dashmachine/paths.py @@ -13,6 +13,10 @@ root_folder = get_root_folder() dashmachine_folder = os.path.join(root_folder, "dashmachine") +platform_folder = os.path.join(dashmachine_folder, "platform") + +user_data_folder = os.path.join(dashmachine_folder, "user_data") + static_folder = os.path.join(dashmachine_folder, "static") images_folder = os.path.join(static_folder, "images") diff --git a/dashmachine/platform/__init__.py b/dashmachine/platform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashmachine/platform/pihole.py b/dashmachine/platform/pihole.py new file mode 100644 index 0000000..b4d24e0 --- /dev/null +++ b/dashmachine/platform/pihole.py @@ -0,0 +1,202 @@ +from flask import render_template_string + + +import requests +import time +import hashlib + + +def inApiLink(ip, endpoint): + return "http://" + str(ip) + "/admin/scripts/pi-hole/php/" + str(endpoint) + ".php" + + +class Auth(object): + def __init__(self, password): + # PiHole's web token is just a double sha256 hash of the utf8 encoded password + self.token = hashlib.sha256( + hashlib.sha256(str(password).encode()).hexdigest().encode() + ).hexdigest() + self.auth_timestamp = time.time() + + +class PiHole(object): + # Takes in an ip address of a pihole server + def __init__(self, ip_address): + self.ip_address = ip_address + self.auth_data = None + self.refresh() + self.pw = None + + def refresh(self): + rawdata = requests.get( + "http://" + self.ip_address + "/admin/api.php?summary" + ).json() + + if self.auth_data != None: + topdevicedata = requests.get( + "http://" + + self.ip_address + + "/admin/api.php?getQuerySources=25&auth=" + + self.auth_data.token + ).json() + + self.top_devices = topdevicedata["top_sources"] + + self.forward_destinations = requests.get( + "http://" + + self.ip_address + + "/admin/api.php?getForwardDestinations&auth=" + + self.auth_data.token + ).json() + + self.query_types = requests.get( + "http://" + + self.ip_address + + "/admin/api.php?getQueryTypes&auth=" + + self.auth_data.token + ).json()["querytypes"] + + # Data that is returned is now parsed into vars + self.status = rawdata["status"] + self.domain_count = rawdata["domains_being_blocked"] + self.queries = rawdata["dns_queries_today"] + self.blocked = rawdata["ads_blocked_today"] + self.ads_percentage = rawdata["ads_percentage_today"] + self.unique_domains = rawdata["unique_domains"] + self.forwarded = rawdata["queries_forwarded"] + self.cached = rawdata["queries_cached"] + self.total_clients = rawdata["clients_ever_seen"] + self.unique_clients = rawdata["unique_clients"] + self.total_queries = rawdata["dns_queries_all_types"] + self.gravity_last_updated = rawdata["gravity_last_updated"] + + def refreshTop(self, count): + if self.auth_data == None: + print("Unable to fetch top items. Please authenticate.") + exit(1) + + rawdata = requests.get( + "http://" + + self.ip_address + + "/admin/api.php?topItems=" + + str(count) + + "&auth=" + + self.auth_data.token + ).json() + self.top_queries = rawdata["top_queries"] + self.top_ads = rawdata["top_ads"] + + def getGraphData(self): + rawdata = requests.get( + "http://" + self.ip_address + "/admin/api.php?overTimeData10mins" + ).json() + return { + "domains": rawdata["domains_over_time"], + "ads": rawdata["ads_over_time"], + } + + def authenticate(self, password): + self.auth_data = Auth(password) + self.pw = password + + # print(self.auth_data.token) + + def getAllQueries(self): + if self.auth_data == None: + print("Unable to get queries. Please authenticate") + exit(1) + return requests.get( + "http://" + + self.ip_address + + "/admin/api.php?getAllQueries&auth=" + + self.auth_data.token + ).json()["data"] + + def enable(self): + if self.auth_data == None: + print("Unable to enable pihole. Please authenticate") + exit(1) + requests.get( + "http://" + + self.ip_address + + "/admin/api.php?enable&auth=" + + self.auth_data.token + ) + + def disable(self, seconds): + if self.auth_data == None: + print("Unable to disable pihole. Please authenticate") + exit(1) + requests.get( + "http://" + + self.ip_address + + "/admin/api.php?disable=" + + str(seconds) + + "&auth=" + + self.auth_data.token + ) + + def getVersion(self): + return requests.get( + "http://" + self.ip_address + "/admin/api.php?versions" + ).json() + + def getDBfilesize(self): + if self.auth_data == None: + print("Please authenticate") + exit(1) + return float( + requests.get( + "http://" + + self.ip_address + + "/admin/api_db.php?getDBfilesize&auth=" + + self.auth_data.token + ).json()["filesize"] + ) + + def getList(self, list): + return requests.get( + inApiLink(self.ip_address, "get") + "?list=" + str(list) + ).json() + + def add(self, list, domain): + if self.auth_data == None: + print("Please authenticate") + exit(1) + with requests.session() as s: + s.get( + "http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/add.php" + ) + requests.post( + "http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/add.php", + data={"list": list, "domain": domain, "pw": self.pw}, + ).text + + def sub(self, list, domain): + if self.auth_data == None: + print("Please authenticate") + exit(1) + with requests.session() as s: + s.get( + "http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/sub.php" + ) + requests.post( + "http://" + str(self.ip_address) + "/admin/scripts/pi-hole/php/sub.php", + data={"list": list, "domain": domain, "pw": self.pw}, + ).text + + +class Platform: + def __init__(self, *args, **kwargs): + # parse the user's options from the config entries + for key, value in kwargs.items(): + self.__dict__[key] = value + + self.pihole = PiHole(self.host) + + def process(self): + self.pihole.refresh() + value_template = render_template_string( + self.value_template, **self.pihole.__dict__ + ) + return value_template diff --git a/dashmachine/platform/ping.py b/dashmachine/platform/ping.py new file mode 100644 index 0000000..f182e5c --- /dev/null +++ b/dashmachine/platform/ping.py @@ -0,0 +1,41 @@ +""" + +##### ping +Check if a service is online. +```ini +[variable_name] +platform = ping +resource = 192.168.1.1 +``` +> **Returns:** a right-aligned colored bullet point on the app card. + +| Variable | Required | Description | Options | +|-----------------|----------|-----------------------------------------------------------------|-------------------| +| [variable_name] | Yes | Name for the data source. | [variable_name] | +| plaform | Yes | Name of the platform. | rest | +| resource | Yes | Url of whatever you want to ping | url | + + +""" + +import platform +import subprocess + + +class Platform: + def __init__(self, *args, **kwargs): + # parse the user's options from the config entries + for key, value in kwargs.items(): + self.__dict__[key] = value + + def process(self): + param = "-n" if platform.system().lower() == "windows" else "-c" + command = ["ping", param, "1", self.resource] + up = subprocess.call(command) == 0 + + if up is True: + icon_class = "theme-success-text" + else: + icon_class = "theme-failure-text" + + return f"fiber_manual_record " diff --git a/dashmachine/platform/rest.py b/dashmachine/platform/rest.py new file mode 100644 index 0000000..ef8825c --- /dev/null +++ b/dashmachine/platform/rest.py @@ -0,0 +1,85 @@ +""" + +##### rest +Make a call on a REST API and display the results as a jinja formatted string. +```ini +[variable_name] +platform = rest +resource = https://your-website.com/api +value_template = {{value}} +method = post +authentication = basic +username = my_username +password = my_password +payload = {"var1": "hi", "var2": 1} +``` +> **Returns:** `value_template` as rendered string + +| Variable | Required | Description | Options | +|-----------------|----------|-----------------------------------------------------------------|-------------------| +| [variable_name] | Yes | Name for the data source. | [variable_name] | +| plaform | Yes | Name of the platform. | rest | +| resource | Yes | Url of rest api resource. | url | +| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template | +| method | No | Method for the api call, default is GET | GET,POST | +| authentication | No | Authentication for the api call, default is None | None,basic,digest | +| username | No | Username to use for auth. | string | +| password | No | Password to use for auth. | string | +| payload | No | Payload for post request. | json | + +> **Working example:** +>```ini +>[test] +>platform = rest +>resource = https://pokeapi.co/api/v2/pokemon +>value_template = Pokemon: {{value['count']}} +> +>[Pokemon] +>prefix = https:// +>url = pokemon.com +>icon = static/images/apps/default.png +>description = Data sources example +>open_in = this_tab +>data_sources = test +>``` + +""" + +import json +from requests import get, post +from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from flask import render_template_string + + +class Platform: + def __init__(self, *args, **kwargs): + # parse the user's options from the config entries + for key, value in kwargs.items(): + self.__dict__[key] = value + + # set defaults for omitted options + if not hasattr(self, "method"): + self.method = "GET" + if not hasattr(self, "authentication"): + self.authentication = None + + def process(self): + if self.method.upper() == "GET": + try: + value = get(self.resource).json() + except Exception as e: + value = f"{e}" + + elif self.method.upper() == "POST": + if self.authentication: + if self.authentication.lower() == "digest": + auth = HTTPDigestAuth(self.username, self.password) + else: + auth = HTTPBasicAuth(self.username, self.password) + else: + auth = None + + payload = json.loads(self.payload.replace("'", '"')) + value = post(self.resource, data=payload, auth=auth) + value_template = render_template_string(self.value_template, value=value) + return value_template diff --git a/dashmachine/platform/transmission.py b/dashmachine/platform/transmission.py new file mode 100644 index 0000000..0fef512 --- /dev/null +++ b/dashmachine/platform/transmission.py @@ -0,0 +1,38 @@ +import json +from flask import render_template_string +import transmissionrpc + + +# from pprint import PrettyPrinter +# pp = PrettyPrinter() + + +class Platform: + def __init__(self, *args, **kwargs): + # parse the user's options from the config entries + for key, value in kwargs.items(): + self.__dict__[key] = value + + if not hasattr(self, "port"): + self.port = 9091 + if not hasattr(self, "host"): + self.host = "localhost" + + self.tc = transmissionrpc.Client( + self.host, port=self.port, user=self.user, password=self.password + ) + + def process(self): + + torrents = len(self.tc.get_torrents()) + data = {} + for key, field in self.tc.session_stats().__dict__["_fields"].items(): + data[key] = field.value + # pp.pprint (data) + + value_template = render_template_string(self.value_template, **data) + return value_template + + +# Testing +# test = Platform(host='192.168.1.19', user='', password='').process() diff --git a/dashmachine/rest_api/resources.py b/dashmachine/rest_api/resources.py index a18f68a..d24173b 100755 --- a/dashmachine/rest_api/resources.py +++ b/dashmachine/rest_api/resources.py @@ -1,5 +1,3 @@ -import os -from flask import request from flask_restful import Resource from dashmachine.version import version @@ -7,15 +5,3 @@ from dashmachine.version import version class GetVersion(Resource): def get(self): return {"Version": version} - - -class ServerShutdown(Resource): - def get(self): - os.system("shutdown now") - return {"Done"} - - -class ServerReboot(Resource): - def get(self): - os.system("reboot") - return {"Done"} diff --git a/dashmachine/settings_system/models.py b/dashmachine/settings_system/models.py index 80f2d94..bc2c7e7 100644 --- a/dashmachine/settings_system/models.py +++ b/dashmachine/settings_system/models.py @@ -6,7 +6,6 @@ class Settings(db.Model): theme = db.Column(db.String()) accent = db.Column(db.String()) background = db.Column(db.String()) - - -db.create_all() -db.session.commit() + roles = db.Column(db.String()) + home_access_groups = db.Column(db.String()) + settings_access_groups = db.Column(db.String()) diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index d447ce7..277f25d 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -1,29 +1,46 @@ import os from shutil import move -from flask import render_template, request, Blueprint, jsonify -from dashmachine.settings_system.forms import ConfigForm +from flask_login import current_user +from flask import render_template, request, Blueprint, jsonify, redirect, url_for from dashmachine.user_system.forms import UserForm from dashmachine.user_system.utils import add_edit_user -from dashmachine.main.utils import read_config, row2dict +from dashmachine.user_system.models import User +from dashmachine.main.utils import row2dict, public_route, check_groups +from dashmachine.main.read_config import read_config from dashmachine.main.models import Files, TemplateApps -from dashmachine.paths import backgrounds_images_folder, icons_images_folder +from dashmachine.settings_system.forms import ConfigForm +from dashmachine.settings_system.utils import load_files_html, get_config_html +from dashmachine.settings_system.models import Settings +from dashmachine.paths import ( + backgrounds_images_folder, + icons_images_folder, + user_data_folder, +) from dashmachine.version import version -from dashmachine.settings_system.utils import load_files_html +from dashmachine import db settings_system = Blueprint("settings_system", __name__) +@public_route @settings_system.route("/settings", methods=["GET"]) def settings(): + settings_db = Settings.query.first() + if not check_groups(settings_db.settings_access_groups, current_user): + return redirect(url_for("main.home")) + config_form = ConfigForm() user_form = UserForm() - with open("dashmachine/user_data/config.ini", "r") as config_file: + with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file: config_form.config.data = config_file.read() files_html = load_files_html() template_apps = [] t_apps = TemplateApps.query.all() for t_app in t_apps: template_apps.append(f"{t_app.name}&&{t_app.icon}") + + users = User.query.all() + config_readme = get_config_html() return render_template( "settings_system/settings.html", config_form=config_form, @@ -31,12 +48,14 @@ def settings(): user_form=user_form, template_apps=",".join(template_apps), version=version, + users=users, + config_readme=config_readme, ) @settings_system.route("/settings/save_config", methods=["POST"]) def save_config(): - with open("dashmachine/user_data/config.ini", "w") as config_file: + with open(os.path.join(user_data_folder, "config.ini"), "w") as config_file: config_file.write(request.form.get("config")) msg = read_config() return jsonify(data=msg) @@ -81,7 +100,14 @@ def edit_user(): if form.validate_on_submit(): if form.password.data != form.confirm_password.data: return jsonify(data={"err": "Passwords don't match"}) - add_edit_user(form.username.data, form.password.data) + err = add_edit_user( + form.username.data, + form.password.data, + user_id=form.id.data, + role=form.role.data, + ) + if err: + return jsonify(data={"err": err}) else: err_str = "" for fieldName, errorMessages in form.errors.items(): @@ -89,4 +115,20 @@ def edit_user(): for err in errorMessages: err_str += f"{err} " return jsonify(data={"err": err_str}) - return jsonify(data={"err": "success"}) + users = User.query.all() + html = render_template("settings_system/user.html", users=users) + return jsonify(data={"err": "success", "html": html}) + + +@settings_system.route("/settings/delete_user", methods=["GET"]) +def delete_user(): + admin_users = User.query.filter_by(role="admin").all() + user = User.query.filter_by(id=request.args.get("id")).first() + if len(admin_users) < 2 and user.role == "admin": + return jsonify(data={"err": "You must have at least one admin user"}) + else: + User.query.filter_by(id=request.args.get("id")).delete() + db.session.commit() + users = User.query.all() + html = render_template("settings_system/user.html", users=users) + return jsonify(data={"err": "success", "html": html}) diff --git a/dashmachine/settings_system/utils.py b/dashmachine/settings_system/utils.py index 57fea69..49baded 100644 --- a/dashmachine/settings_system/utils.py +++ b/dashmachine/settings_system/utils.py @@ -1,11 +1,43 @@ -from dashmachine.paths import backgrounds_images_folder, icons_images_folder +import os +import importlib +from markdown2 import markdown +from dashmachine.paths import ( + backgrounds_images_folder, + icons_images_folder, + root_folder, + platform_folder, +) from flask import render_template -from os import listdir def load_files_html(): - backgrounds = listdir(backgrounds_images_folder) - icons = listdir(icons_images_folder) + backgrounds = os.listdir(backgrounds_images_folder) + icons = os.listdir(icons_images_folder) return render_template( "settings_system/files.html", backgrounds=backgrounds, icons=icons, ) + + +def get_config_html(): + with open(os.path.join(root_folder, "config_readme.md")) as readme_file: + md = readme_file.read() + platforms = os.listdir(platform_folder) + platforms = sorted(platforms) + for platform in platforms: + name, extension = os.path.splitext(platform) + if extension.lower() == ".py": + module = importlib.import_module(f"dashmachine.platform.{name}", ".") + if module.__doc__: + md += module.__doc__ + + config_html = markdown( + md, + extras=[ + "tables", + "fenced-code-blocks", + "break-on-newline", + "header-ids", + "code-friendly", + ], + ) + return config_html diff --git a/dashmachine/sources.py b/dashmachine/sources.py index a470d8f..3926c8b 100644 --- a/dashmachine/sources.py +++ b/dashmachine/sources.py @@ -1,8 +1,10 @@ import os import random from jsmin import jsmin +from flask_login import current_user from dashmachine import app from dashmachine.main.models import Apps +from dashmachine.main.utils import check_groups from dashmachine.settings_system.models import Settings from dashmachine.paths import static_folder, backgrounds_images_folder from dashmachine.cssmin import cssmin @@ -72,13 +74,23 @@ def process_css_sources(process_bundle=None, src=None, app_global=False): @app.context_processor def context_processor(): - apps = Apps.query.all() + apps = [] + apps_db = Apps.query.all() + for app_db in apps_db: + if not app_db.groups: + app_db.groups = None + if check_groups(app_db.groups, current_user): + apps.append(app_db) + settings = Settings.query.first() if settings.background == "random": - settings.background = ( - f"static/images/backgrounds/" - f"{random.choice(os.listdir(backgrounds_images_folder))}" - ) + if len(os.listdir(backgrounds_images_folder)) < 1: + settings.background = None + else: + settings.background = ( + f"static/images/backgrounds/" + f"{random.choice(os.listdir(backgrounds_images_folder))}" + ) return dict( test_key="test", process_js_sources=process_js_sources, diff --git a/dashmachine/static/css/global/dashmachine-theme.css b/dashmachine/static/css/global/dashmachine-theme.css index 318e1d3..cd9f62f 100644 --- a/dashmachine/static/css/global/dashmachine-theme.css +++ b/dashmachine/static/css/global/dashmachine-theme.css @@ -11,7 +11,9 @@ --theme-color-font: #2c2f3a; --theme-color-font-muted: rgba(44, 47, 58, 0.85); --theme-color-font-muted2: rgba(44, 47, 58, 0.65); - --theme-warning: #f44336; + --theme-failure: #f44336; + --theme-warning: #ffae42; + --theme-success: #4BB543; --theme-on-primary: #fff; } [data-theme="dark"] { @@ -117,12 +119,24 @@ .theme-text { color: var(--theme-color-font) !important; } +.theme-failure { + background-color: var(--theme-failure) !important; +} +.theme-failure-text { + color: var(--theme-failure) !important; +} .theme-warning { background-color: var(--theme-warning) !important; } .theme-warning-text { color: var(--theme-warning) !important; } +.theme-success { + background-color: var(--theme-success) !important; +} +.theme-success-text { + color: var(--theme-success) !important; +} .theme-muted-text { color: var(--theme-color-font-muted) !important; } diff --git a/dashmachine/static/css/global/dashmachine.css b/dashmachine/static/css/global/dashmachine.css index e8c14b2..710877e 100644 --- a/dashmachine/static/css/global/dashmachine.css +++ b/dashmachine/static/css/global/dashmachine.css @@ -28,6 +28,9 @@ .no-vis { visibility: hidden; } +.filtered { + display: none !important; +} .scrollbar { overflow-y: scroll !important; diff --git a/dashmachine/static/css/main/home.css b/dashmachine/static/css/main/home.css new file mode 100644 index 0000000..65d2e61 --- /dev/null +++ b/dashmachine/static/css/main/home.css @@ -0,0 +1,23 @@ + +.tags-select-col { + position: relative; + top: 15px; + margin: 0; + border-radius: .4rem; + height: 45px; + -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12); +} +.tags-select-col .select-wrapper { + top: 5px; +} +.tags-select-col .select-wrapper input { + color: var(--theme-secondary); +} +@media screen and (max-width: 992px) { + .tags-select-col { + top: 0; + width: calc(100vw - 45px) !important; + margin-left: 15px !important; + } +} \ No newline at end of file diff --git a/dashmachine/static/css/settings_system/settings.css b/dashmachine/static/css/settings_system/settings.css index 293b145..5d71fc3 100644 --- a/dashmachine/static/css/settings_system/settings.css +++ b/dashmachine/static/css/settings_system/settings.css @@ -15,4 +15,40 @@ border-top-left-radius: 0px; border-top-right-radius: 0px; background: var(--theme-surface-1); +} + +#config-readme h5 { + color: var(--theme-primary); + margin-top: 5%; +} +#config-readme h4 { + color: var(--theme-color-font-muted); + margin-top: 5%; +} +#configini-readme { + margin-top: 2% !important; +} +#config-readme code { + -webkit-touch-callout: all; + -webkit-user-select: all; + -khtml-user-select: all; + -moz-user-select: all; + -ms-user-select: all; + user-select: all; + cursor: text; +} +#config-readme th { + color: var(--theme-primary); +} +#config-readme td { + -webkit-touch-callout: text !important; + -webkit-user-select: text !important; + -khtml-user-select: text !important; + -moz-user-select: text !important; + -ms-user-select: text !important; + user-select: text !important; + cursor: text; +} +#config-readme strong { + font-weight: 900; } \ No newline at end of file diff --git a/dashmachine/static/js/main/home.js b/dashmachine/static/js/main/home.js index c4bba42..664916b 100644 --- a/dashmachine/static/js/main/home.js +++ b/dashmachine/static/js/main/home.js @@ -15,15 +15,26 @@ $( document ).ready(function() { }); }); - $(".data-template").each(function(e) { + $(".data-source-container").each(function(e) { var el = $(this); $.ajax({ url: el.attr('data-url'), type: 'GET', - data: {template: el.text()}, + data: {id: el.attr('data-id')}, success: function(data){ - el.text(data); - el.removeClass('hide'); + el.closest('.col').find('.data-source-loading').addClass('hide'); + el.html(data); + } + }); + }); + + $("#tags-select").on('change', function(e) { + var value = $(this).val(); + $(".app-a").each(function(i, e) { + if ($(this).attr("data-tags").indexOf(value) > -1 || value === "All tags") { + $(this).removeClass('filtered'); + } else { + $(this).addClass('filtered'); } }); }); diff --git a/dashmachine/static/js/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index 3ce6514..99950f8 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -3,7 +3,11 @@ d.className += " active theme-primary"; $( document ).ready(function() { initTCdrop('#images-tcdrop'); - $("#config-wiki-modal").modal(); + $("#user-modal").modal({ + onCloseEnd: function () { + $("#edit-user-form").trigger('reset'); + } + }); $("#save-config-btn").on('click', function(e) { $.ajax({ @@ -15,7 +19,7 @@ $( document ).ready(function() { M.toast({html: 'Config applied successfully'}); location.reload(true); } else { - M.toast({html: data.data.msg, classes: "theme-warning"}); + M.toast({html: data.data.msg, classes: "theme-failure"}); } } }); @@ -58,21 +62,22 @@ $( document ).ready(function() { } }); - $("#edit-user-btn").on('click', function(e) { - $.ajax({ - url: $(this).attr('data-url'), - type: 'POST', - data: $("#edit-user-form").serialize(), - success: function(data){ - if (data.data.err !== 'success'){ - M.toast({html: data.data.err, classes: 'theme-warning'}); - } else { - $("#user-form-password").val(''); - $("#user-form-confirm_password").val(''); - M.toast({html: 'User updated'}); - } - } - }); + $("#save-user-btn").on('click', function(e) { + $.ajax({ + url: $(this).attr('data-url'), + type: 'POST', + data: $("#edit-user-form").serialize(), + success: function(data){ + if (data.data.err !== 'success'){ + M.toast({html: data.data.err, classes: 'theme-failure'}); + } else { + $("#users-div").empty(); + $("#users-div").append(data.data.html); + $("#user-modal").modal('close'); + M.toast({html: 'User saved'}); + } + } + }); }); }); \ No newline at end of file diff --git a/dashmachine/templates/error_pages/unauthorized.html b/dashmachine/templates/error_pages/unauthorized.html new file mode 100644 index 0000000..d45df2e --- /dev/null +++ b/dashmachine/templates/error_pages/unauthorized.html @@ -0,0 +1,11 @@ +{% extends "main/base.html" %} + + +{% block content %} +
+
+ warning +

Unauthorized

+
+
+{% endblock content %} diff --git a/dashmachine/templates/global_macros.html b/dashmachine/templates/global_macros.html index b5b3d5b..e063677 100644 --- a/dashmachine/templates/global_macros.html +++ b/dashmachine/templates/global_macros.html @@ -169,10 +169,13 @@ col_style=None id='', form_obj=None, size="s12", -label='' +label=None, +class='' ) %}
- {{ form_obj(id=id) }} - + {{ form_obj(id=id, class=class, placeholder="Tags") }} + {% if label %} + + {% endif %}
{% endmacro %} \ No newline at end of file diff --git a/dashmachine/templates/main/home.html b/dashmachine/templates/main/home.html index ec54380..ce62a6d 100755 --- a/dashmachine/templates/main/home.html +++ b/dashmachine/templates/main/home.html @@ -1,10 +1,11 @@ {% extends "main/layout.html" %} -{% from 'global_macros.html' import data %} +{% from 'global_macros.html' import data, preload_circle, select %} {% block page_vendor_css %} {% endblock page_vendor_css %} {% block page_lvl_css %} + {{ process_css_sources(src="main/home.css")|safe }} {% if settings.background and settings.background != 'None' %} +{% macro FilesTab() %} +
+
Images
+
+
+ + +
+ +
+
+ {{ tcdrop(allowed_types='jpg,jpeg,png,gif', id="images-tcdrop", max_files="30") }} + {{ button(text="save", icon="save", id="save-images-btn", float="left", data={"url": url_for('settings_system.add_images')}) }} +
+
-
-
-
-
    -
  • Backgrounds
  • - {% if backgrounds %} - {% for background in backgrounds %} -
  • - - - - static/images/backgrounds/{{ background }} - +
    +
    {{ files_html|safe }}
    +
    +{% endmacro %} + +{% macro Files(icons, backgrounds) %} + + +
    +
    +
    +
      +
    • Backgrounds
    • + {% if backgrounds %} + {% for background in backgrounds %} +
    • + + + + static/images/backgrounds/{{ background }} + close - filter_none -
    • - {% endfor %} - {% else %} -
    • No files yet
    • - {% endif %} -
    + filter_none +
  • + {% endfor %} + {% else %} +
  • No files yet
  • + {% endif %} +
+
- -
-
-
-
    -
  • Icons
  • - {% if icons %} - {% for icon in icons %} -
  • - - - - static/images/icons/{{ icon }} - +
    +
    +
    +
      +
    • Icons
    • + {% if icons %} + {% for icon in icons %} +
    • + + + + static/images/icons/{{ icon }} + close - filter_none -
    • - {% endfor %} - {% else %} -
    • No files yet
    • - {% endif %} -
    + filter_none +
  • + {% endfor %} + {% else %} +
  • No files yet
  • + {% endif %} +
+
- - \ No newline at end of file + +{% endmacro %} + +{{ Files(icons, backgrounds) }} \ No newline at end of file diff --git a/dashmachine/templates/settings_system/settings.html b/dashmachine/templates/settings_system/settings.html index e3fda11..2990c23 100644 --- a/dashmachine/templates/settings_system/settings.html +++ b/dashmachine/templates/settings_system/settings.html @@ -1,6 +1,8 @@ {% extends "main/layout.html" %} -{% from 'global_macros.html' import input, button %} +{% from 'global_macros.html' import input, button, select %} {% from 'main/tcdrop.html' import tcdrop %} +{% from 'settings_system/user.html' import UserTab with context %} +{% from 'settings_system/files.html' import FilesTab with context %} {% block page_vendor_css %} {% endblock page_vendor_css %} @@ -19,234 +21,6 @@ {% endblock page_lvl_css %} {% block content %} -
@@ -256,11 +30,7 @@
-
Config - - info - -
+
Config.ini
{{ button( icon="save", id="save-config-btn", @@ -292,6 +62,10 @@
-
-
Images
-
-
- - -
- -
-
- {{ tcdrop(allowed_types='jpg,jpeg,png,gif', id="images-tcdrop", max_files="30") }} - {{ button(text="save", icon="save", id="save-images-btn", float="left", data={"url": url_for('settings_system.add_images')}) }} -
-
- -
-
{{ files_html|safe }}
-
- + {{ FilesTab() }}
@@ -350,54 +103,15 @@
+ +
+ +
+ {{ config_readme|safe }}
-
-
User
- -
- {{ user_form.hidden_tag() }} - - {{ input( - label="Username", - id="user-form-username", - size="s12", - form_obj=user_form.username, - val=current_user.username - ) }} - - {{ input( - label="Password", - id="user-form-password", - form_obj=user_form.password, - size="s12" - ) }} - - {{ input( - label="Confirm Password", - id="user-form-confirm_password", - form_obj=user_form.confirm_password, - required='required', - size="s12" - ) }} -
- - {{ button( - icon="save", - float="left", - id="edit-user-btn", - data={'url': url_for('settings_system.edit_user')}, - text="save" - ) }} -
- -
-
DashMachine
-

version: {{ version }}

- -
- + {{ UserTab() }}
diff --git a/dashmachine/templates/settings_system/user.html b/dashmachine/templates/settings_system/user.html new file mode 100644 index 0000000..99992e2 --- /dev/null +++ b/dashmachine/templates/settings_system/user.html @@ -0,0 +1,130 @@ +{% macro UserTab() %} + + +
+
+
Users + + add + +
+ +
+ {{ Users(users) }} +
+ +
+
+ + +
+
DashMachine
+

version: {{ version }}

+
+{% endmacro %} + +{% macro Users(users) %} + {% for user in users %} +
+
+ + {{ user.username }} + {{ user.role }} + + + edit + close + +
+
+ {% endfor %} + +{% endmacro %} + +{{Users(users)}} \ No newline at end of file diff --git a/dashmachine/user_system/forms.py b/dashmachine/user_system/forms.py old mode 100755 new mode 100644 index 7372c67..e36ca0b --- a/dashmachine/user_system/forms.py +++ b/dashmachine/user_system/forms.py @@ -1,10 +1,9 @@ from flask_wtf import FlaskForm -from wtforms import ( - StringField, - PasswordField, - BooleanField, -) +from wtforms import StringField, PasswordField, BooleanField, SelectField from wtforms.validators import DataRequired, Length +from dashmachine.settings_system.models import Settings + +settings_db = Settings.query.first() class UserForm(FlaskForm): @@ -12,6 +11,16 @@ class UserForm(FlaskForm): password = PasswordField(validators=[DataRequired(), Length(min=8, max=120)]) + role = SelectField(choices=[(role, role) for role in settings_db.roles.split(",")]) + + id = StringField() + confirm_password = PasswordField() + +class LoginForm(FlaskForm): + username = StringField(validators=[DataRequired(), Length(min=1, max=120)]) + + password = PasswordField(validators=[DataRequired(), Length(min=8, max=120)]) + remember = BooleanField() diff --git a/dashmachine/user_system/models.py b/dashmachine/user_system/models.py index 32c75e9..ba3ef34 100644 --- a/dashmachine/user_system/models.py +++ b/dashmachine/user_system/models.py @@ -11,7 +11,4 @@ class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(60), nullable=False) - - -db.create_all() -db.session.commit() + role = db.Column(db.String()) diff --git a/dashmachine/user_system/routes.py b/dashmachine/user_system/routes.py index 5437d85..00d367e 100755 --- a/dashmachine/user_system/routes.py +++ b/dashmachine/user_system/routes.py @@ -1,6 +1,6 @@ from flask import render_template, url_for, redirect, Blueprint -from flask_login import login_user, logout_user, current_user -from dashmachine.user_system.forms import UserForm +from flask_login import login_user, logout_user +from dashmachine.user_system.forms import LoginForm from dashmachine.user_system.models import User from dashmachine.user_system.utils import add_edit_user from dashmachine import bcrypt @@ -18,10 +18,7 @@ user_system = Blueprint("user_system", __name__) def login(): user = User.query.first() - if current_user.is_authenticated: - return redirect(url_for("main.home")) - - form = UserForm() + form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data.lower()).first() diff --git a/dashmachine/user_system/utils.py b/dashmachine/user_system/utils.py index 9b981de..4855319 100755 --- a/dashmachine/user_system/utils.py +++ b/dashmachine/user_system/utils.py @@ -2,16 +2,21 @@ from dashmachine import db, bcrypt from dashmachine.user_system.models import User -def add_edit_user(username, password, user_id=None): +def add_edit_user(username, password, user_id=None, role=None): if user_id: user = User.query.filter_by(id=user_id).first() + if not user: + user = User() else: - user = User.query.first() - if not user: user = User() + admin_users = User.query.filter_by(role="admin").all() + if user_id and role != "admin" and len(admin_users) < 2: + return "You must have at least one admin user" + hashed_password = bcrypt.generate_password_hash(password).decode("utf-8") user.username = username user.password = hashed_password + user.role = role db.session.merge(user) db.session.commit() diff --git a/dashmachine/version.py b/dashmachine/version.py index 35520dc..fc8a2fa 100755 --- a/dashmachine/version.py +++ b/dashmachine/version.py @@ -1 +1 @@ -version = "v0.22" +version = "v0.3" diff --git a/default_config.ini b/default_config.ini index c768f39..6654307 100644 --- a/default_config.ini +++ b/default_config.ini @@ -1,4 +1,7 @@ [Settings] theme = light accent = orange -background = None \ No newline at end of file +background = None +roles = admin,user,public_user +home_access_groups = admin_only +settings_access_groups = admin_only \ No newline at end of file diff --git a/migrations/versions/01a575cda54d_.py b/migrations/versions/01a575cda54d_.py new file mode 100644 index 0000000..cb5a6b7 --- /dev/null +++ b/migrations/versions/01a575cda54d_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 01a575cda54d +Revises: 598477dd1193 +Create Date: 2020-02-04 07:39:43.504475 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "01a575cda54d" +down_revision = "598477dd1193" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("apps", sa.Column("groups", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("apps", "groups") + # ### end Alembic commands ### diff --git a/migrations/versions/03663c18575b_.py b/migrations/versions/03663c18575b_.py new file mode 100644 index 0000000..6ef9788 --- /dev/null +++ b/migrations/versions/03663c18575b_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 03663c18575b +Revises: af72304ae017 +Create Date: 2020-02-04 07:14:23.184567 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "03663c18575b" +down_revision = "af72304ae017" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("user", sa.Column("role", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("user", "role") + # ### end Alembic commands ### diff --git a/migrations/versions/45ebff47af9f_.py b/migrations/versions/45ebff47af9f_.py new file mode 100644 index 0000000..bc1e504 --- /dev/null +++ b/migrations/versions/45ebff47af9f_.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: 45ebff47af9f +Revises: 6bd40f00f2eb +Create Date: 2020-02-06 11:48:22.563926 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "45ebff47af9f" +down_revision = "6bd40f00f2eb" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("api_calls") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "api_calls", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("name", sa.VARCHAR(), nullable=True), + sa.Column("resource", sa.VARCHAR(), nullable=True), + sa.Column("method", sa.VARCHAR(), nullable=True), + sa.Column("payload", sa.VARCHAR(), nullable=True), + sa.Column("authentication", sa.VARCHAR(), nullable=True), + sa.Column("username", sa.VARCHAR(), nullable=True), + sa.Column("password", sa.VARCHAR(), nullable=True), + sa.Column("value_template", sa.VARCHAR(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### diff --git a/migrations/versions/598477dd1193_.py b/migrations/versions/598477dd1193_.py new file mode 100644 index 0000000..c62c87e --- /dev/null +++ b/migrations/versions/598477dd1193_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 598477dd1193 +Revises: 03663c18575b +Create Date: 2020-02-04 07:33:25.019173 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "598477dd1193" +down_revision = "03663c18575b" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("settings", sa.Column("roles", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("settings", "roles") + # ### end Alembic commands ### diff --git a/migrations/versions/6bd40f00f2eb_.py b/migrations/versions/6bd40f00f2eb_.py new file mode 100644 index 0000000..daad2fc --- /dev/null +++ b/migrations/versions/6bd40f00f2eb_.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: 6bd40f00f2eb +Revises: d87e35114b0b +Create Date: 2020-02-05 18:41:57.209232 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "6bd40f00f2eb" +down_revision = "d87e35114b0b" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("apps", sa.Column("groups", sa.String(), nullable=True)) + op.add_column( + "settings", sa.Column("home_access_groups", sa.String(), nullable=True) + ) + op.add_column("settings", sa.Column("roles", sa.String(), nullable=True)) + op.add_column( + "settings", sa.Column("settings_access_groups", sa.String(), nullable=True) + ) + op.add_column("user", sa.Column("role", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("user", "role") + op.drop_column("settings", "settings_access_groups") + op.drop_column("settings", "roles") + op.drop_column("settings", "home_access_groups") + op.drop_column("apps", "groups") + # ### end Alembic commands ### diff --git a/migrations/versions/885c5f9b33d5_.py b/migrations/versions/885c5f9b33d5_.py new file mode 100644 index 0000000..a60db60 --- /dev/null +++ b/migrations/versions/885c5f9b33d5_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 885c5f9b33d5 +Revises: 8f5a046465e8 +Create Date: 2020-02-08 13:30:01.632487 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "885c5f9b33d5" +down_revision = "8f5a046465e8" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("apps", sa.Column("tags", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("apps", "tags") + # ### end Alembic commands ### diff --git a/migrations/versions/8f5a046465e8_.py b/migrations/versions/8f5a046465e8_.py new file mode 100644 index 0000000..42d6172 --- /dev/null +++ b/migrations/versions/8f5a046465e8_.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: 8f5a046465e8 +Revises: 45ebff47af9f +Create Date: 2020-02-06 19:51:14.594434 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "8f5a046465e8" +down_revision = "45ebff47af9f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("api_calls") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "api_calls", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("name", sa.VARCHAR(), nullable=True), + sa.Column("resource", sa.VARCHAR(), nullable=True), + sa.Column("method", sa.VARCHAR(), nullable=True), + sa.Column("payload", sa.VARCHAR(), nullable=True), + sa.Column("authentication", sa.VARCHAR(), nullable=True), + sa.Column("username", sa.VARCHAR(), nullable=True), + sa.Column("password", sa.VARCHAR(), nullable=True), + sa.Column("value_template", sa.VARCHAR(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### diff --git a/migrations/versions/d87e35114b0b_.py b/migrations/versions/d87e35114b0b_.py new file mode 100644 index 0000000..6b18fd0 --- /dev/null +++ b/migrations/versions/d87e35114b0b_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: d87e35114b0b +Revises: 01a575cda54d +Create Date: 2020-02-04 08:13:35.783741 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "d87e35114b0b" +down_revision = "01a575cda54d" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "settings", sa.Column("home_access_groups", sa.String(), nullable=True) + ) + op.add_column( + "settings", sa.Column("settings_access_groups", sa.String(), nullable=True) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("settings", "settings_access_groups") + op.drop_column("settings", "home_access_groups") + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 2033357..99c7509 100755 --- a/requirements.txt +++ b/requirements.txt @@ -32,3 +32,5 @@ SQLAlchemy==1.3.13 urllib3==1.25.8 Werkzeug==0.16.1 WTForms==2.2.1 +transmissionrpc +markdown2 \ No newline at end of file