Merge branch 'develop'

This commit is contained in:
Ross Mountjoy 2020-02-08 17:14:27 -05:00
commit f3cf929cc5
49 changed files with 1571 additions and 630 deletions

View File

@ -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
@ -59,14 +61,3 @@ If you change the config.ini file, you either have to restart the container (or
* 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

85
config_readme.md Normal file
View File

@ -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`

View File

@ -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__)

View File

@ -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")

View File

@ -0,0 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import SelectField
class TagsForm(FlaskForm):
tags = SelectField(choices=[("All tags", "All tags")])

View File

@ -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())

View File

@ -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)}

View File

@ -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?<url>", methods=["GET"])
def app_view(url):
return render_template("main/app-view.html", url=f"https://{url}")
@public_route
@main.route("/app_view?<app_id>", 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
# ------------------------------------------------------------------------------

View File

@ -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()
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()

View File

@ -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")

View File

View File

@ -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

View File

@ -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"<i class='material-icons right {icon_class}'>fiber_manual_record </i>"

View File

@ -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

View File

@ -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()

View File

@ -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"}

View File

@ -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())

View File

@ -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})

View File

@ -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

View File

@ -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,9 +74,19 @@ 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":
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))}"

View File

@ -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;
}

View File

@ -28,6 +28,9 @@
.no-vis {
visibility: hidden;
}
.filtered {
display: none !important;
}
.scrollbar {
overflow-y: scroll !important;

View File

@ -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;
}
}

View File

@ -16,3 +16,39 @@
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;
}

View File

@ -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');
}
});
});

View File

@ -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,18 +62,19 @@ $( document ).ready(function() {
}
});
$("#edit-user-btn").on('click', function(e) {
$("#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-warning'});
M.toast({html: data.data.err, classes: 'theme-failure'});
} else {
$("#user-form-password").val('');
$("#user-form-confirm_password").val('');
M.toast({html: 'User updated'});
$("#users-div").empty();
$("#users-div").append(data.data.html);
$("#user-modal").modal('close');
M.toast({html: 'User saved'});
}
}
});

View File

@ -0,0 +1,11 @@
{% extends "main/base.html" %}
{% block content %}
<div class="row center-align mt-10">
<div class="col s12">
<i class="material-icons-outlined theme-failure-text" style="font-size: 8rem">warning</i>
<h4>Unauthorized</h4>
</div>
</div>
{% endblock content %}

View File

@ -169,10 +169,13 @@ col_style=None
id='',
form_obj=None,
size="s12",
label=''
label=None,
class=''
) %}
<div class="input-field col {{size}}">
{{ form_obj(id=id) }}
{{ form_obj(id=id, class=class, placeholder="Tags") }}
{% if label %}
<label>{{ label }}</label>
{% endif %}
</div>
{% endmacro %}

View File

@ -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' %}
<style>
#main {
@ -27,31 +28,40 @@
<input type="text" id="apps-filter" class="card-filter theme-surface-transparent" placeholder="Search apps">
</span>
</div>
{% if tags_form.tags.choices|count > 1 %}
<div class="input-field col s12 l2 tags-select-col theme-surface-transparent">
{{ tags_form.tags(id='tags-select') }}
</div>
{% endif %}
</div>
<div class="row">
{% if apps %}
{% for app in apps %}
{% if app.open_in == 'iframe' %}
<a href="{{ url_for('main.app_view', url=app.url) }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}">
<a href="{{ url_for('main.app_view', app_id=app.id) }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}" data-tags="{{ app.tags }}">
{% elif app.open_in == 'this_tab' %}
<a href="{{ app.prefix }}{{ app.url }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}">
<a href="{{ app.prefix }}{{ app.url }}" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}" data-tags="{{ app.tags }}">
{% elif app.open_in == "new_tab" %}
<a href="{{ app.prefix }}{{ app.url }}" target="_blank" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}">
<a href="{{ app.prefix }}{{ app.url }}" target="_blank" class="app-a" data-name="{{ app.name }}" data-description="{{ app.description }}" data-tags="{{ app.tags }}">
{% endif %}
<div class="col s12 m6 l3">
<div class="card theme-surface-transparent app-card">
<div class="card-content center-align scrollbar" style="max-height: 118px; min-height: 118px; scrollbar-width: none;">
{% if app.data_template %}
{% if app.data_sources.count() > 0 %}
<div class="row">
<div class="col s6 center-align">
<img src="{{ app.icon }}" height="64px">
</div>
<div class="col s6 left-align">
<p class="data-template hide theme-text" data-url="{{ url_for('main.load_rest_data') }}" style="white-space: pre-line">
{{ app.data_template|safe }}
<span class="data-source-loading">{{ preload_circle() }}</span>
{% for data_source in app.data_sources %}
<p class="data-source-container"
data-url="{{ url_for('main.load_data_source') }}"
data-id="{{ data_source.id }}">
</p>
{% endfor %}
</div>
</div>
{% else %}
@ -59,7 +69,8 @@
{% endif %}
</div>
<div class="card-action center-align scrollbar" style="max-height: 127px; min-height: 127px; scrollbar-width: none;">
<h5>{{ app.name }}</h5>
<h5>{{ app.name }}
</h5>
<span class="theme-secondary-text">{{ app.description }}</span>
</div>
</div>

View File

@ -24,10 +24,17 @@
<span class="menu-title" data-i18n="">Settings</span>
</a></li>
{% if current_user.is_authenticated %}
<li class="bold"><a id="logout-sidenav" class="waves-effect waves-cyan" href="{{ url_for('user_system.logout') }}">
<i class="material-icons-outlined">exit_to_app</i>
<span class="menu-title" data-i18n="">Logout</span>
</a></li>
{% else %}
<li class="bold"><a id="logout-sidenav" class="waves-effect waves-cyan" href="{{ url_for('user_system.login') }}">
<i class="material-icons-outlined">account_circle</i>
<span class="menu-title" data-i18n="">Login</span>
</a></li>
{% endif %}
<li class="bold"><a id="hide-sidenav" class="waves-effect waves-cyan" href="#">
<i class="material-icons-outlined">menu_open</i>
@ -37,7 +44,7 @@
{% for app in apps %}
<li class="bold">
{% if app.open_in == 'iframe' %}
<a id="dashboard-sidenav" class="waves-effect waves-cyan" href="{{ url_for('main.app_view', url=app.url) }}">
<a id="dashboard-sidenav" class="waves-effect waves-cyan" href="{{ url_for('main.app_view', app_id=app.id) }}">
{% elif app.open_in == "this_tab" %}
<a id="dashboard-sidenav" class="waves-effect waves-cyan" href="{{ app.prefix }}{{ app.url }}">
{% elif app.open_in == "new_tab" %}

View File

@ -16,8 +16,8 @@
{{ preload_circle() }}
</li>
<li class="tcdrop-error-msg tcdrop-li hide">
<i class="tcdrop-delete-file material-icons theme-warning">error</i>
<span class="tcdrop-error-msg-txt file-name theme-warning-text"></span>
<i class="tcdrop-delete-file material-icons theme-failure">error</i>
<span class="tcdrop-error-msg-txt file-name theme-failure-text"></span>
</li>
</ul>
</div>

View File

@ -1,3 +1,28 @@
{% macro FilesTab() %}
<div class="row">
<h5>Images</h5>
<form id="add-images-form">
<div class="input-field col s12 mt-4">
<select name="folder">
<option value="icons">Icons</option>
<option value="backgrounds">Backgrounds</option>
</select>
<label>Folder</label>
</div>
<input name="files" id="add-images-input" class="hide">
</form>
<div class="col s12">
{{ 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')}) }}
</div>
</div>
<div class="row">
<div id="files-div">{{ files_html|safe }}</div>
</div>
{% endmacro %}
{% macro Files(icons, backgrounds) %}
<style>
.file-title {
position: relative;
@ -76,3 +101,6 @@
});
});
</script>
{% endmacro %}
{{ Files(icons, backgrounds) }}

View File

@ -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 %}
<div id="config-wiki-modal" class="modal full-height-modal">
<div class="modal-content">
<div class="row">
<div class="col s12">
<h4>Config.ini Readme
<span><i class="material-icons-outlined modal-close right icon-btn">close</i></span>
</h4>
</div>
<div id="config-help-col" class="col s12 theme-surface-1 padding-2 border-radius-10">
<h5>Settings Reference</h5>
<code class="selectable-all">
[Settings]<br>
theme = dark<br>
accent = orange<br>
background = static/images/backgrounds/background.png<br>
</code>
<table class="mt-4 responsive-table">
<thead>
<tr>
<th>Variable</th>
<th>Required</th>
<th>Description</th>
<th>Options</th>
</tr>
</thead>
<tbody class="selectable">
<tr>
<td>[Settings]</td>
<td>Yes</td>
<td>Config section name.</td>
<td>string</td>
</tr>
<tr>
<td>theme</td>
<td>Yes</td>
<td>UI theme</td>
<td>light, dark</td>
</tr>
<tr>
<td>accent</td>
<td>Yes</td>
<td>UI accent color</td>
<td>
orange, red, pink, purple, deepPurple, indigo, blue, lightBlue,
cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey
</td>
</tr>
<tr>
<td>background</td>
<td>Yes</td>
<td>Background image for the UI</td>
<td>/static/images/backgrounds/yourpicture.png, external link to image, None, random</td>
</tr>
</tbody>
</table>
<h5>App Reference</h5>
<code class="selectable-all">
[App Name]<br>
prefix = https://<br>
url = your-website.com<br>
icon = static/images/apps/default.png<br>
sidebar_icon = static/images/apps/default.png<br>
description = Example description<br>
open_in = iframe<br>
data_template = None
</code>
<table class="mt-4 responsive-table">
<thead>
<tr>
<th>Variable</th>
<th>Required</th>
<th>Description</th>
<th>Options</th>
</tr>
</thead>
<tbody class="selectable">
<tr>
<td>[App Name]</td>
<td>Yes</td>
<td>The name of your app.</td>
<td>string</td>
</tr>
<tr>
<td>prefix</td>
<td>Yes</td>
<td>The prefix for the app's url.</td>
<td>web prefix, e.g. http:// or https://</td>
</tr>
<tr>
<td>url</td>
<td>Yes</td>
<td>The url for your app.</td>
<td>web url, e.g. myapp.com</td>
</tr>
<tr>
<td>icon</td>
<td>No</td>
<td>Icon for the dashboard.</td>
<td>/static/images/icons/yourpicture.png, external link to image</td>
</tr>
<tr>
<td>sidebar_icon</td>
<td>No</td>
<td>Icon for the sidenav.</td>
<td>/static/images/icons/yourpicture.png, external link to image</td>
</tr>
<tr>
<td>description</td>
<td>No</td>
<td>A short description for the app.</td>
<td>string</td>
</tr>
<tr>
<td>open_in</td>
<td>Yes</td>
<td>open the app in the current tab, an iframe or a new tab</td>
<td>iframe, new_tab, this_tab</td>
</tr>
<tr>
<td>data_template</td>
<td>No</td>
<td>Template for displaying variable(s) from rest data *Note: you must have a rest data variable set up in the config</td>
<td>example: Data: {{ '{{ your_variable }}' }}</td>
</tr>
</tbody>
</table>
<h5>Api Data Reference</h5>
<code class="selectable-all">
[variable_name]<br>
platform = rest<br>
resource = your-website.com<br>
value_template = variable_name<br>
</code>
<table class="mt-4 responsive-table">
<thead>
<tr>
<th>Variable</th>
<th>Required</th>
<th>Description</th>
<th>Options</th>
</tr>
</thead>
<tbody class="selectable">
<tr>
<td>[variable_name]</td>
<td>Yes</td>
<td>The variable to be made available to apps.</td>
<td>variable (python syntax)</td>
</tr>
<tr>
<td>platform</td>
<td>Yes</td>
<td>Platform for data source</td>
<td>rest</td>
</tr>
<tr>
<td>resource</td>
<td>Yes</td>
<td>The url for the api call.</td>
<td>myapp.com/api/hello</td>
</tr>
<tr>
<td>value_template</td>
<td>No</td>
<td>Tranform the data returned by the api call (python syntax)</td>
<td>variable_name[0]['info']</td>
</tr>
<tr>
<td>method</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
</tr>
<tr>
<td>payload</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
</tr>
<tr>
<td>authentication</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
</tr>
<tr>
<td>username</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
</tr>
<tr>
<td>password</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
<td>NOT IMPLEMENTED</td>
</tr>
</tbody>
</table>
<h5>Api Data Example</h5>
<p>Say we wanted to display how many Pokemon there are using the PokeAPI, we would add the following to the config:</p>
<code class="selectable-all">
[num_pokemon]<br>
platform = rest<br>
resource = https://pokeapi.co/api/v2/pokemon<br>
value_template = num_pokemon['count']<br>
</code>
<p>Then in the config entry for the app you want to add this to, you would add:</p>
<code class="selectable-all">
data_template = Pokemon: {{ '{{ num_pokemon }}' }}
</code>
</div>
</div>
</div>
</div>
<div id="main" class="main-full">
<div class="container">
@ -256,11 +30,7 @@
<div class="card-content">
<div class="row">
<div class="col s12">
<h5>Config
<a href="#config-wiki-modal" class="modal-trigger">
<i class="material-icons-outlined theme-secondary-text icon-btn ml-2 toggle-config-help" style="position: relative; top: 4px;">info</i>
</a>
</h5>
<h5>Config.ini</h5>
{{ button(
icon="save",
id="save-config-btn",
@ -292,6 +62,10 @@
<div class="row">
<div class="col s12 mb-2">
<ul class="tabs tabs-fixed-width">
<li class="tab col s3"><a href="#config-readme">
<i class="material-icons-outlined">info</i>
</a></li>
<li class="tab col s3"><a href="#images">
<i class="material-icons-outlined">photo_library</i>
</a></li>
@ -307,28 +81,7 @@
</div>
<div id="images" class="col s12">
<div class="row">
<h5>Images</h5>
<form id="add-images-form">
<div class="input-field col s12 mt-4">
<select name="folder">
<option value="icons">Icons</option>
<option value="backgrounds">Backgrounds</option>
</select>
<label>Folder</label>
</div>
<input name="files" id="add-images-input" class="hide">
</form>
<div class="col s12">
{{ 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')}) }}
</div>
</div>
<div class="row">
<div id="files-div">{{ files_html|safe }}</div>
</div>
{{ FilesTab() }}
</div>
<div id="apps" class="col s12">
@ -350,54 +103,15 @@
<div id="template-div" class="selectable-all code"></div>
</div>
</div>
</div>
<div id="config-readme" class="col s12">
{{ config_readme|safe }}
</div>
<div id="user" class="col s12">
<div class="row">
<h5>User</h5>
<form id="edit-user-form">
{{ 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"
) }}
</form>
{{ button(
icon="save",
float="left",
id="edit-user-btn",
data={'url': url_for('settings_system.edit_user')},
text="save"
) }}
</div>
<div class="row mt-4">
<h5>DashMachine</h5>
<p class="mb-2">version: {{ version }}</p>
</div>
{{ UserTab() }}
</div>
</div>

View File

@ -0,0 +1,130 @@
{% macro UserTab() %}
<div id="user-modal" class="modal">
<div class="modal-content">
<div class="row mt-2">
<div class="col s12">
<form id="edit-user-form">
{{ user_form.hidden_tag() }}
{{ select(
id='user-form-role',
form_obj=user_form.role,
size="s12",
label='Role'
) }}
{{ input(
label="Username",
id="user-form-username",
size="s12",
form_obj=user_form.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"
) }}
{{ user_form.id(class="hide", id="user-form-id") }}
</form>
{{ button(
icon="save",
float="left",
id="save-user-btn",
class="mb-2",
data={'url': url_for('settings_system.edit_user')},
text="save"
) }}
</div>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col s12">
<h5>Users
<a href="#user-modal" class="modal-trigger">
<i class="material-icons-outlined theme-secondary-text icon-btn ml-2 toggle-config-help" style="position: relative; top: 4px;">add</i>
</a>
</h5>
<div id="users-div">
{{ Users(users) }}
</div>
</div>
</div>
<div class="row mt-4">
<h5>DashMachine</h5>
<p class="mb-2">version: {{ version }}</p>
</div>
{% endmacro %}
{% macro Users(users) %}
{% for user in users %}
<div class="card theme-surface-1">
<div class="card-content">
<span style="font-size: 1.3rem">
{{ user.username }}
<span class="theme-secondary-text">{{ user.role }}</span>
</span>
<span class="right pb-2">
<i class="material-icons-outlined icon-btn edit-user-btn"
data-role="{{ user.role }}"
data-id="{{ user.id }}"
data-username="{{ user.username }}">edit</i>
<i class="material-icons-outlined icon-btn delete-user-btn"
data-id="{{ user.id }}"
data-url="{{ url_for('settings_system.delete_user') }}">close</i>
</span>
</div>
</div>
{% endfor %}
<script>
$(".edit-user-btn").on('click', function(e) {
$("#user-modal").modal('open');
$("#user-form-username").val($(this).attr("data-username"));
$("#user-form-role").val($(this).attr("data-role"));
$("#user-form-id").val($(this).attr("data-id"));
M.updateTextFields();
});
$(".delete-user-btn").on('click', function(e) {
var r = confirm("Are you sure?");
if (r == true) {
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {id: $(this).attr('data-id')},
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);
$("#edit-user-modal").modal('close');
M.toast({html: 'User deleted'});
}
}
});
}
});
</script>
{% endmacro %}
{{Users(users)}}

19
dashmachine/user_system/forms.py Executable file → Normal file
View File

@ -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()

View File

@ -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())

View File

@ -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()

View File

@ -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()
else:
user = User.query.first()
if not user:
user = User()
else:
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()

View File

@ -1 +1 @@
version = "v0.22"
version = "v0.3"

View File

@ -2,3 +2,6 @@
theme = light
accent = orange
background = None
roles = admin,user,public_user
home_access_groups = admin_only
settings_access_groups = admin_only

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -32,3 +32,5 @@ SQLAlchemy==1.3.13
urllib3==1.25.8
Werkzeug==0.16.1
WTForms==2.2.1
transmissionrpc
markdown2