From 4fe3cb657579b071fe6a23d82326f8713aa2df03 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Tue, 4 Feb 2020 08:40:36 -0500 Subject: [PATCH 01/26] Started work on 1.0 added roles/access groups --- dashmachine/main/models.py | 7 +- dashmachine/main/utils.py | 155 +++++++++++++++++--------- dashmachine/settings_system/models.py | 7 +- dashmachine/user_system/models.py | 5 +- dashmachine/user_system/utils.py | 3 +- default_config.ini | 29 ++++- migrations/versions/01a575cda54d_.py | 28 +++++ migrations/versions/03663c18575b_.py | 28 +++++ migrations/versions/598477dd1193_.py | 28 +++++ migrations/versions/d87e35114b0b_.py | 34 ++++++ 10 files changed, 259 insertions(+), 65 deletions(-) create mode 100644 migrations/versions/01a575cda54d_.py create mode 100644 migrations/versions/03663c18575b_.py create mode 100644 migrations/versions/598477dd1193_.py create mode 100644 migrations/versions/d87e35114b0b_.py diff --git a/dashmachine/main/models.py b/dashmachine/main/models.py index 8a6f749..7b61141 100644 --- a/dashmachine/main/models.py +++ b/dashmachine/main/models.py @@ -20,6 +20,7 @@ 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()) class TemplateApps(db.Model): @@ -45,5 +46,7 @@ class ApiCalls(db.Model): value_template = db.Column(db.String()) -db.create_all() -db.session.commit() +class Groups(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String()) + roles = db.Column(db.String()) diff --git a/dashmachine/main/utils.py b/dashmachine/main/utils.py index a144ad5..b9b6706 100755 --- a/dashmachine/main/utils.py +++ b/dashmachine/main/utils.py @@ -4,7 +4,7 @@ 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 Apps, ApiCalls, TemplateApps, Groups from dashmachine.settings_system.models import Settings from dashmachine.user_system.models import User from dashmachine.user_system.utils import add_edit_user @@ -29,66 +29,100 @@ def read_config(): 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}."} + Groups.query.delete() 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." - } + # Settings creation + if section == "Settings": + settings = Settings() + if "theme" in config["Settings"]: + settings.theme = config["Settings"]["theme"] + else: + settings.theme = "light" - if "method" in config[section]: - api_call.method = config[section]["method"] - else: - api_call.method = "GET" + if "accent" in config["Settings"]: + settings.accent = config["Settings"]["accent"] + else: + settings.accent = "orange" - if "payload" in config[section]: - api_call.payload = config[section]["payload"] - else: - api_call.payload = None + if "background" in config["Settings"]: + settings.background = config["Settings"]["background"] + else: + settings.background = "None" - if "authentication" in config[section]: - api_call.authentication = config[section]["authentication"] - else: - api_call.authentication = None + if "roles" in config["Settings"]: + settings.roles = config["Settings"]["roles"] + else: + settings.roles = "admin" - if "username" in config[section]: - api_call.username = config[section]["username"] - else: - api_call.username = None + if "home_access_groups" in config["Settings"]: + settings.home_access_groups = config["Settings"]["home_access_groups"] + else: + settings.home_access_groups = "admin_only" - if "password" in config[section]: - api_call.password = config[section]["password"] - else: - api_call.password = None + if "settings_access_groups" in config["Settings"]: + settings.settings_access_groups = config["Settings"][ + "settings_access_groups" + ] + else: + settings.settings_access_groups = "admin_only" - if "value_template" in config[section]: - api_call.value_template = config[section]["value_template"] - else: - api_call.value_template = section + db.session.add(settings) + db.session.commit() - db.session.add(api_call) - db.session.commit() - continue + # Groups creation + elif "roles" in config[section]: + group = Groups() + group.name = section + group.roles = config[section]["roles"] + db.session.add(group) + db.session.commit() + # API call creation + elif "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 + + else: # App creation app = Apps() app.name = section @@ -127,6 +161,11 @@ def read_config(): else: app.data_template = None + if "groups" in config[section]: + app.groups = config[section]["groups"] + else: + app.groups = None + db.session.add(app) db.session.commit() return {"msg": "success", "settings": row2dict(settings)} @@ -165,13 +204,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,11 +236,17 @@ 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(), + ) def get_rest_data(template): 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/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/utils.py b/dashmachine/user_system/utils.py index 9b981de..6b6ef0c 100755 --- a/dashmachine/user_system/utils.py +++ b/dashmachine/user_system/utils.py @@ -2,7 +2,7 @@ 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: @@ -13,5 +13,6 @@ def add_edit_user(username, password, user_id=None): 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/default_config.ini b/default_config.ini index c768f39..172cb3d 100644 --- a/default_config.ini +++ b/default_config.ini @@ -1,4 +1,31 @@ +# -------- +# SETTINGS +# -------- [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 + +# ------------- +# ACCESS GROUPS +# ------------- +[public] +roles = admin, user, public_user + +[private] +roles = admin, user + +[admin_only] +roles = admin + +# -------- +# API DATA +# -------- + + +# ---- +# APPS +# ---- \ No newline at end of file diff --git a/migrations/versions/01a575cda54d_.py b/migrations/versions/01a575cda54d_.py new file mode 100644 index 0000000..0c61a57 --- /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..c96460f --- /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/598477dd1193_.py b/migrations/versions/598477dd1193_.py new file mode 100644 index 0000000..2c4e0d5 --- /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/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 ### From 800f99b79c0bfa16fe8f8a30c0f18e977fa04662 Mon Sep 17 00:00:00 2001 From: kv Date: Tue, 4 Feb 2020 15:20:59 +0000 Subject: [PATCH 02/26] Update 'dashmachine/user_system/forms.py' Making change to 1 character minimum for username --- dashmachine/user_system/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 dashmachine/user_system/forms.py diff --git a/dashmachine/user_system/forms.py b/dashmachine/user_system/forms.py old mode 100755 new mode 100644 index 3edf26b..7372c67 --- a/dashmachine/user_system/forms.py +++ b/dashmachine/user_system/forms.py @@ -8,7 +8,7 @@ from wtforms.validators import DataRequired, Length class UserForm(FlaskForm): - username = StringField(validators=[DataRequired(), Length(min=4, max=120)]) + username = StringField(validators=[DataRequired(), Length(min=1, max=120)]) password = PasswordField(validators=[DataRequired(), Length(min=8, max=120)]) From 84d2b4bac7bd64c71f3d06c3c6e0b51718b49dd1 Mon Sep 17 00:00:00 2001 From: kv Date: Tue, 4 Feb 2020 15:32:03 +0000 Subject: [PATCH 03/26] Upload files to 'dashmachine/static/images/apps' Adding riot.png --- dashmachine/static/images/apps/riot.png | Bin 0 -> 12907 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 dashmachine/static/images/apps/riot.png diff --git a/dashmachine/static/images/apps/riot.png b/dashmachine/static/images/apps/riot.png new file mode 100644 index 0000000000000000000000000000000000000000..f68d6e25d31c6894790109ed6c75239cd1f8d502 GIT binary patch literal 12907 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 Date: Tue, 4 Feb 2020 15:33:12 +0000 Subject: [PATCH 04/26] Upload files to 'dashmachine/static/images/apps' Adding jellyfin.png --- dashmachine/static/images/apps/jellyfin.png | Bin 0 -> 17495 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 dashmachine/static/images/apps/jellyfin.png diff --git a/dashmachine/static/images/apps/jellyfin.png b/dashmachine/static/images/apps/jellyfin.png new file mode 100644 index 0000000000000000000000000000000000000000..d30455b8f1cf74fd67399723358aea7348ccdd81 GIT binary patch literal 17495 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 D+a>4) literal 0 HcmV?d00001 From 3f45f8d79bd28a5675f484410f5c722f035e3ca9 Mon Sep 17 00:00:00 2001 From: kv Date: Tue, 4 Feb 2020 15:59:07 +0000 Subject: [PATCH 05/26] Update 'app_templates.ini' Adding Riot, Jellyfin, Lidarr, Airsonic, Tautulli, Bazarr, Ombi, Syncthing and The Lounge templates --- app_templates.ini | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/app_templates.ini b/app_templates.ini index e830836..613476c 100644 --- a/app_templates.ini +++ b/app_templates.ini @@ -81,3 +81,66 @@ url = 192.168.39.175:9117 icon = static/images/apps/jackett.png description = API Support for your favorite torrent trackers open_in = this_tab + +[Riot] +prefix = https:// +url = your-website.com +icon = static/images/apps/riot.png +description = A glossy Matrix collaboration client for the web +open_in = this_tab + +[Jellyfin] +prefix = https:// +url = your-website.com +icon = static/images/apps/jellyfin.png +description = The Free Software Media System +open_in = this_tab + +[Lidarr] +prefix = http:// +url = 192.168.39.175:8686 +icon = static/images/apps/jellyfin.png +description = Looks and smells like Sonarr but made for music +open_in = this_tab + +[Airsonic] +prefix = http:// +url = 192.168.39.175:4040 +icon = static/images/apps/jellyfin.png +description = A Free and Open Source community driven media server +open_in = this_tab + +[Tautulli] +prefix = http:// +url = 192.168.39.175:8181 +icon = static/images/apps/jellyfin.png +description = A Python based monitoring and tracking tool for Plex Media Server +open_in = this_tab + +[Bazarr] +prefix = http:// +url = 192.168.39.175:6767 +icon = static/images/apps/jellyfin.png +description = A companion application to Sonarr and Radarr +open_in = this_tab + +[Ombi] +prefix = http:// +url = 192.168.39.175:3579 +icon = static/images/apps/jellyfin.png +description = Want a Movie or TV Show on Plex or Emby? Use Ombi! +open_in = this_tab + +[Syncthing] +prefix = http:// +url = 192.168.39.175:8384 +icon = static/images/apps/jellyfin.png +description = Open Source Continuous File Synchronization +open_in = this_tab + +[The Lounge] +prefix = http:// +url = 192.168.39.175:9000 +icon = static/images/apps/jellyfin.png +description = Modern, responsive, cross-platform, self-hosted web IRC client +open_in = this_tab From 77748ecc0b6191eab084c86163f8d147e80dc274 Mon Sep 17 00:00:00 2001 From: kv Date: Tue, 4 Feb 2020 16:01:39 +0000 Subject: [PATCH 06/26] Upload files to 'dashmachine/static/images/apps' Adding ombi, the lounge, traefik, lidarr, syncthing --- dashmachine/static/images/apps/Syncthing.png | Bin 0 -> 42257 bytes dashmachine/static/images/apps/lidarr.png | Bin 0 -> 44323 bytes dashmachine/static/images/apps/ombi.png | Bin 0 -> 10140 bytes dashmachine/static/images/apps/thelounge.png | Bin 0 -> 7246 bytes dashmachine/static/images/apps/traefik.png | Bin 0 -> 35056 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 dashmachine/static/images/apps/Syncthing.png create mode 100644 dashmachine/static/images/apps/lidarr.png create mode 100644 dashmachine/static/images/apps/ombi.png create mode 100644 dashmachine/static/images/apps/thelounge.png create mode 100644 dashmachine/static/images/apps/traefik.png diff --git a/dashmachine/static/images/apps/Syncthing.png b/dashmachine/static/images/apps/Syncthing.png new file mode 100644 index 0000000000000000000000000000000000000000..1e9f32735a392a8a61a69084985a54051985c088 GIT binary patch literal 42257 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 u1T|N*zaE5ggI%j7+Jy1 z!Iy({$+@&1_;nJFWEweej*qAimL=noZ_XBA9uhJ#!YnJH8hvu+%X#CO+sNcs8K*Pz$DG zj=%O(VU&tYL9c|K8`I}t4i7k?(X{2JPY0uS7#dI6?G@TSFweFbVT*d6tk6}yo6u|m zOr-h|?xIO{=_4Qx)E(La;19b=rZm$Ih94qb(qJ1W1RMzGNSC8r@oCwXB#H=>EA~9{ zL%y8p{>8#U)PKzTq##XPY*`*4JF~Coj}8#lchm`LTL|Z6k3_tXmEnDM9z`P0D_g#+J_HJ?^7%;`Prsf%0Ald5!sq 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 z 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 literal 0 HcmV?d00001 diff --git a/dashmachine/static/images/apps/ombi.png b/dashmachine/static/images/apps/ombi.png new file mode 100644 index 0000000000000000000000000000000000000000..8094bfdda062ea104600d8117ca675f36e2a2171 GIT binary patch literal 10140 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 I9c=vn0ivcx5&!@I literal 0 HcmV?d00001 diff --git a/dashmachine/static/images/apps/thelounge.png b/dashmachine/static/images/apps/thelounge.png new file mode 100644 index 0000000000000000000000000000000000000000..543e7b0191ad870336d7e0164dfff14614a5da2c GIT binary patch literal 7246 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 meeHLrd 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 Date: Tue, 4 Feb 2020 16:04:03 +0000 Subject: [PATCH 07/26] Upload files to 'dashmachine/static/images/apps' Adding bazarr, airsonic and tautulli --- dashmachine/static/images/apps/airsonic.png | Bin 0 -> 3274 bytes dashmachine/static/images/apps/bazarr.png | Bin 0 -> 15324 bytes dashmachine/static/images/apps/tautulli.png | Bin 0 -> 12724 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 dashmachine/static/images/apps/airsonic.png create mode 100644 dashmachine/static/images/apps/bazarr.png create mode 100644 dashmachine/static/images/apps/tautulli.png diff --git a/dashmachine/static/images/apps/airsonic.png b/dashmachine/static/images/apps/airsonic.png new file mode 100644 index 0000000000000000000000000000000000000000..363f387543ccc417ab29d5c5f023a17ec97ebdd7 GIT binary patch literal 3274 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 literal 0 HcmV?d00001 diff --git a/dashmachine/static/images/apps/bazarr.png b/dashmachine/static/images/apps/bazarr.png new file mode 100644 index 0000000000000000000000000000000000000000..9bcaa2966aa1c8c44c20ededa4f8939aac2066fd GIT binary patch literal 15324 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 Date: Tue, 4 Feb 2020 16:06:49 +0000 Subject: [PATCH 08/26] Update 'app_templates.ini' Fixing up png links and adding traefik --- app_templates.ini | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app_templates.ini b/app_templates.ini index 613476c..3041f3f 100644 --- a/app_templates.ini +++ b/app_templates.ini @@ -99,48 +99,55 @@ open_in = this_tab [Lidarr] prefix = http:// url = 192.168.39.175:8686 -icon = static/images/apps/jellyfin.png +icon = static/images/apps/lidarr.png description = Looks and smells like Sonarr but made for music open_in = this_tab [Airsonic] prefix = http:// url = 192.168.39.175:4040 -icon = static/images/apps/jellyfin.png +icon = static/images/apps/airsonic.png description = A Free and Open Source community driven media server open_in = this_tab [Tautulli] prefix = http:// url = 192.168.39.175:8181 -icon = static/images/apps/jellyfin.png +icon = static/images/apps/tautulli.png description = A Python based monitoring and tracking tool for Plex Media Server open_in = this_tab [Bazarr] prefix = http:// url = 192.168.39.175:6767 -icon = static/images/apps/jellyfin.png +icon = static/images/apps/bazarr.png description = A companion application to Sonarr and Radarr open_in = this_tab [Ombi] prefix = http:// url = 192.168.39.175:3579 -icon = static/images/apps/jellyfin.png +icon = static/images/apps/ombi.png description = Want a Movie or TV Show on Plex or Emby? Use Ombi! open_in = this_tab [Syncthing] prefix = http:// url = 192.168.39.175:8384 -icon = static/images/apps/jellyfin.png +icon = static/images/apps/Syncthing.png description = Open Source Continuous File Synchronization open_in = this_tab [The Lounge] prefix = http:// url = 192.168.39.175:9000 -icon = static/images/apps/jellyfin.png +icon = static/images/apps/thelounge.png description = Modern, responsive, cross-platform, self-hosted web IRC client open_in = this_tab + +[Traefik] +prefix = http:// +url = 192.168.39.175:8080 +icon = static/images/apps/traefik.png +description = The Cloud Native Edge Router +open_in = this_tab From 1194b543c5a7d5b33498d29440fa793488ab4765 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Wed, 5 Feb 2020 09:26:59 -0500 Subject: [PATCH 09/26] worked on access groups/roles --- dashmachine/main/read_config.py | 180 +++++++++++ dashmachine/main/routes.py | 7 +- dashmachine/main/utils.py | 182 ++---------- dashmachine/paths.py | 2 + dashmachine/rest_api/resources.py | 14 - dashmachine/settings_system/routes.py | 26 +- dashmachine/sources.py | 11 +- .../settings_system/config-readme.html | 279 ++++++++++++++++++ .../templates/settings_system/settings.html | 224 +------------- dashmachine/user_system/routes.py | 3 - dashmachine/user_system/utils.py | 4 +- default_config.ini | 29 +- 12 files changed, 531 insertions(+), 430 deletions(-) create mode 100644 dashmachine/main/read_config.py create mode 100644 dashmachine/templates/settings_system/config-readme.html diff --git a/dashmachine/main/read_config.py b/dashmachine/main/read_config.py new file mode 100644 index 0000000..af5cbbc --- /dev/null +++ b/dashmachine/main/read_config.py @@ -0,0 +1,180 @@ +import os +from configparser import ConfigParser +from dashmachine.main.models import Apps, ApiCalls, Groups +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}."} + + Apps.query.delete() + ApiCalls.query.delete() + Settings.query.delete() + Groups.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() + + # API call creation + elif "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 + + 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 "data_template" in config[section]: + app.data_template = config[section]["data_template"] + else: + app.data_template = None + + if "groups" in config[section]: + app.groups = config[section]["groups"] + else: + app.groups = None + + db.session.add(app) + db.session.commit() + + 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..7e95273 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -5,7 +5,8 @@ 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.utils import get_rest_data, public_route, check_groups +from dashmachine.settings_system.models import Settings from dashmachine.paths import cache_folder from dashmachine import app, db @@ -49,9 +50,13 @@ def check_valid_login(): # ------------------------------------------------------------------------------ # /home # ------------------------------------------------------------------------------ +@public_route @main.route("/") @main.route("/home", methods=["GET", "POST"]) def home(): + settings = Settings.query.first() + if not check_groups(settings.home_access_groups, current_user): + return redirect(url_for("user_system.login")) return render_template("main/home.html") diff --git a/dashmachine/main/utils.py b/dashmachine/main/utils.py index b9b6706..6cb2a45 100755 --- a/dashmachine/main/utils.py +++ b/dashmachine/main/utils.py @@ -4,7 +4,8 @@ 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, Groups +from dashmachine.main.models import ApiCalls, 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,158 +20,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() - Groups.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"] - else: - settings.roles = "admin" - - 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() - - # API call creation - elif "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 - - 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 "data_template" in config[section]: - app.data_template = config[section]["data_template"] - else: - app.data_template = None - - if "groups" in config[section]: - app.groups = config[section]["groups"] - else: - app.groups = None - - db.session.add(app) - db.session.commit() - return {"msg": "success", "settings": row2dict(settings)} - - def read_template_apps(): config = ConfigParser() try: @@ -248,6 +97,11 @@ def dashmachine_init(): 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: @@ -267,3 +121,25 @@ def do_api_call(key): exec(f"{key} = {value.json()}") value = str(eval(api_call.value_template)) return value + + +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 diff --git a/dashmachine/paths.py b/dashmachine/paths.py index 7397a4f..d5e6bb0 100755 --- a/dashmachine/paths.py +++ b/dashmachine/paths.py @@ -13,6 +13,8 @@ root_folder = get_root_folder() dashmachine_folder = os.path.join(root_folder, "dashmachine") +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/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/routes.py b/dashmachine/settings_system/routes.py index d447ce7..9000053 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -1,23 +1,35 @@ 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.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.version import version +from dashmachine.settings_system.forms import ConfigForm from dashmachine.settings_system.utils import load_files_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 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 = [] @@ -36,7 +48,7 @@ def settings(): @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) diff --git a/dashmachine/sources.py b/dashmachine/sources.py index a470d8f..e521fa1 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,7 +74,14 @@ 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 = ( diff --git a/dashmachine/templates/settings_system/config-readme.html b/dashmachine/templates/settings_system/config-readme.html new file mode 100644 index 0000000..04f431d --- /dev/null +++ b/dashmachine/templates/settings_system/config-readme.html @@ -0,0 +1,279 @@ +{% macro ConfigReadme() %} +
+

Config.ini Readme + close +

+
+
+
Settings
+ + [Settings]
+ theme = dark
+ accent = orange
+ background = static/images/backgrounds/background.png
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableRequiredDescriptionOptions
[Settings]YesConfig section name.string
themeYesUI themelight, dark
accentYesUI accent color + orange, red, pink, purple, deepPurple, indigo, blue, lightBlue, + cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey +
backgroundYesBackground image for the UI/static/images/backgrounds/yourpicture.png, external link to image, None, random
rolesNoUser roles for access groups.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_groupsNoDefine which access groups can access the /home pageRoles defined in your config. If not defined, default is admin_only
settings_access_groupsNoDefine which access groups can access the /settings pageRoles defined in your config. If not defined, default is admin_only
+ +
Apps
+ + [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_template = None +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableRequiredDescriptionOptions
[App Name]YesThe name of your app.string
prefixYesThe prefix for the app's url.web prefix, e.g. http:// or https://
urlYesThe url for your app.web url, e.g. myapp.com
iconNoIcon for the dashboard./static/images/icons/yourpicture.png, external link to image
sidebar_iconNoIcon for the sidenav./static/images/icons/yourpicture.png, external link to image
descriptionNoA short description for the app.string
open_inYesopen the app in the current tab, an iframe or a new tabiframe, new_tab, this_tab
data_templateNoTemplate for displaying variable(s) from rest data *Note: you must have a rest data variable set up in the configexample: Data: {{ '{{ your_variable }}' }}
+ +
Access Groups
+ + + [public]
+ roles = admin, user, public_user
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
VariableRequiredDescriptionOptions
[Group Name]YesName for access groupstring
rolesYesA comma separated list of user roles allowed to view apps in this access groupRoles defined in your config. If not defined, defaults are admin and 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' + + +
Api Data
+ + [variable_name]
+ platform = rest
+ resource = your-website.com
+ value_template = variable_name
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableRequiredDescriptionOptions
[variable_name]YesThe variable to be made available to apps.variable (python syntax)
platformYesPlatform for data sourcerest
resourceYesThe url for the api call.myapp.com/api/hello
value_templateNoTranform the data returned by the api call (python syntax)variable_name[0]['info']
methodNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
payloadNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
authenticationNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
usernameNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
passwordNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
+ +
Api Data Example
+

Say we wanted to display how many Pokemon there are using the PokeAPI, we would add the following to the config:

+ + [num_pokemon]
+ platform = rest
+ resource = https://pokeapi.co/api/v2/pokemon
+ value_template = num_pokemon['count']
+
+ +

Then in the config entry for the app you want to add this to, you would add:

+ + + data_template = Pokemon: {{ '{{ num_pokemon }}' }} + + +
+{% endmacro %} \ No newline at end of file diff --git a/dashmachine/templates/settings_system/settings.html b/dashmachine/templates/settings_system/settings.html index e3fda11..01e9fd4 100644 --- a/dashmachine/templates/settings_system/settings.html +++ b/dashmachine/templates/settings_system/settings.html @@ -1,6 +1,7 @@ {% extends "main/layout.html" %} {% from 'global_macros.html' import input, button %} {% from 'main/tcdrop.html' import tcdrop %} +{% from 'settings_system/config-readme.html' import ConfigReadme %} {% block page_vendor_css %} {% endblock page_vendor_css %} @@ -22,228 +23,7 @@ diff --git a/dashmachine/user_system/routes.py b/dashmachine/user_system/routes.py index 5437d85..6ee494a 100755 --- a/dashmachine/user_system/routes.py +++ b/dashmachine/user_system/routes.py @@ -18,9 +18,6 @@ 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() if form.validate_on_submit(): diff --git a/dashmachine/user_system/utils.py b/dashmachine/user_system/utils.py index 6b6ef0c..e01bf9e 100755 --- a/dashmachine/user_system/utils.py +++ b/dashmachine/user_system/utils.py @@ -2,9 +2,11 @@ from dashmachine import db, bcrypt from dashmachine.user_system.models import User -def add_edit_user(username, password, user_id=None, role=None): +def add_edit_user(username, password, user_id=None, role=None, new=False): if user_id: user = User.query.filter_by(id=user_id).first() + elif new: + user = User() else: user = User.query.first() if not user: diff --git a/default_config.ini b/default_config.ini index 172cb3d..c768f39 100644 --- a/default_config.ini +++ b/default_config.ini @@ -1,31 +1,4 @@ -# -------- -# SETTINGS -# -------- [Settings] theme = light accent = orange -background = None -roles = admin, user, public_user -home_access_groups = admin_only -settings_access_groups = admin_only - -# ------------- -# ACCESS GROUPS -# ------------- -[public] -roles = admin, user, public_user - -[private] -roles = admin, user - -[admin_only] -roles = admin - -# -------- -# API DATA -# -------- - - -# ---- -# APPS -# ---- \ No newline at end of file +background = None \ No newline at end of file From d934878f5a439004e8c8fe7e6963ff3e7761849c Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Thu, 6 Feb 2020 06:48:27 -0500 Subject: [PATCH 10/26] starting working on plugin/platform system --- dashmachine/platform/__init__.py | 0 dashmachine/platform/rest.py | 6 +++++ migrations/versions/01a575cda54d_.py | 8 +++--- migrations/versions/03663c18575b_.py | 8 +++--- migrations/versions/598477dd1193_.py | 8 +++--- migrations/versions/6bd40f00f2eb_.py | 40 ++++++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 dashmachine/platform/__init__.py create mode 100644 dashmachine/platform/rest.py create mode 100644 migrations/versions/6bd40f00f2eb_.py diff --git a/dashmachine/platform/__init__.py b/dashmachine/platform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashmachine/platform/rest.py b/dashmachine/platform/rest.py new file mode 100644 index 0000000..ebf0b98 --- /dev/null +++ b/dashmachine/platform/rest.py @@ -0,0 +1,6 @@ +class Platform: + def __init__(self, *args, **kwargs): + pass + + def process(self): + pass diff --git a/migrations/versions/01a575cda54d_.py b/migrations/versions/01a575cda54d_.py index 0c61a57..cb5a6b7 100644 --- a/migrations/versions/01a575cda54d_.py +++ b/migrations/versions/01a575cda54d_.py @@ -10,19 +10,19 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '01a575cda54d' -down_revision = '598477dd1193' +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)) + 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') + op.drop_column("apps", "groups") # ### end Alembic commands ### diff --git a/migrations/versions/03663c18575b_.py b/migrations/versions/03663c18575b_.py index c96460f..6ef9788 100644 --- a/migrations/versions/03663c18575b_.py +++ b/migrations/versions/03663c18575b_.py @@ -10,19 +10,19 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '03663c18575b' -down_revision = 'af72304ae017' +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)) + 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("user", "role") # ### end Alembic commands ### diff --git a/migrations/versions/598477dd1193_.py b/migrations/versions/598477dd1193_.py index 2c4e0d5..c62c87e 100644 --- a/migrations/versions/598477dd1193_.py +++ b/migrations/versions/598477dd1193_.py @@ -10,19 +10,19 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '598477dd1193' -down_revision = '03663c18575b' +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)) + 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') + 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 ### From 90897b9668e0abf2f43de7c3bc3e6a1edb558380 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Thu, 6 Feb 2020 06:54:07 -0500 Subject: [PATCH 11/26] fixing https://git.wolf-house.net/ross/DashMachine/issues/12 --- dashmachine/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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__) From e3dd6f2a681ced29148111e4605786ef348eb437 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Thu, 6 Feb 2020 06:57:30 -0500 Subject: [PATCH 12/26] fixing https://git.wolf-house.net/ross/DashMachine/issues/14 --- dashmachine/sources.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dashmachine/sources.py b/dashmachine/sources.py index e521fa1..3926c8b 100644 --- a/dashmachine/sources.py +++ b/dashmachine/sources.py @@ -84,10 +84,13 @@ def context_processor(): 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, From b4764d104621d5dc8aab80fbbb458420d2c5366f Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Thu, 6 Feb 2020 07:12:47 -0500 Subject: [PATCH 13/26] fixing https://git.wolf-house.net/ross/DashMachine/issues/15 --- dashmachine/main/routes.py | 9 +++++---- dashmachine/templates/main/home.html | 2 +- dashmachine/templates/main/layout.html | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index 7e95273..9037548 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -4,7 +4,7 @@ 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.models import Files, Apps from dashmachine.main.utils import get_rest_data, public_route, check_groups from dashmachine.settings_system.models import Settings from dashmachine.paths import cache_folder @@ -60,9 +60,10 @@ def home(): return render_template("main/home.html") -@main.route("/app_view?", methods=["GET"]) -def app_view(url): - return render_template("main/app-view.html", url=f"https://{url}") +@main.route("/app_view?", methods=["GET"]) +def app_view(app_id): + 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"]) diff --git a/dashmachine/templates/main/home.html b/dashmachine/templates/main/home.html index ec54380..766c392 100755 --- a/dashmachine/templates/main/home.html +++ b/dashmachine/templates/main/home.html @@ -33,7 +33,7 @@ {% for app in apps %} {% if app.open_in == 'iframe' %} -
+ {% elif app.open_in == 'this_tab' %} {% elif app.open_in == "new_tab" %} diff --git a/dashmachine/templates/main/layout.html b/dashmachine/templates/main/layout.html index 585c4ec..ccea715 100644 --- a/dashmachine/templates/main/layout.html +++ b/dashmachine/templates/main/layout.html @@ -37,7 +37,7 @@ {% for app in apps %}
  • {% if app.open_in == 'iframe' %} - + {% elif app.open_in == "this_tab" %} {% elif app.open_in == "new_tab" %} From 43ae3103e1e9371b8e0ae4c63ca2b97b412280a2 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Thu, 6 Feb 2020 10:57:45 -0500 Subject: [PATCH 14/26] started work on the platform/plugin system --- dashmachine/main/models.py | 25 +++++++++ dashmachine/main/read_config.py | 77 +++++++++++----------------- dashmachine/main/routes.py | 16 +++++- dashmachine/main/utils.py | 13 +++++ dashmachine/platform/rest.py | 47 +++++++++++++++-- dashmachine/static/js/main/home.js | 13 +++++ dashmachine/templates/main/home.html | 14 +++-- 7 files changed, 149 insertions(+), 56 deletions(-) diff --git a/dashmachine/main/models.py b/dashmachine/main/models.py index 7b61141..276a920 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) @@ -46,6 +52,25 @@ class ApiCalls(db.Model): value_template = db.Column(db.String()) +class DataSources(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = 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"), + ) + + +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()) diff --git a/dashmachine/main/read_config.py b/dashmachine/main/read_config.py index af5cbbc..26e652f 100644 --- a/dashmachine/main/read_config.py +++ b/dashmachine/main/read_config.py @@ -1,6 +1,6 @@ import os from configparser import ConfigParser -from dashmachine.main.models import Apps, ApiCalls, Groups +from dashmachine.main.models import Apps, ApiCalls, Groups, DataSources, DataSourcesArgs from dashmachine.settings_system.models import Settings from dashmachine.paths import user_data_folder from dashmachine import db @@ -21,6 +21,11 @@ def read_config(): 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() ApiCalls.query.delete() Settings.query.delete() @@ -80,48 +85,21 @@ def read_config(): db.session.add(group) db.session.commit() - # API call creation + # Data source creation elif "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) + data_source = DataSources() + data_source.name = section + data_source.platform = config[section]["platform"] + db.session.add(data_source) db.session.commit() - continue + 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 @@ -157,11 +135,6 @@ def read_config(): else: app.open_in = "this_tab" - if "data_template" in config[section]: - app.data_template = config[section]["data_template"] - else: - app.data_template = None - if "groups" in config[section]: app.groups = config[section]["groups"] else: @@ -170,6 +143,18 @@ def read_config(): 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() diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index 9037548..546191a 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -4,8 +4,13 @@ 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, Apps -from dashmachine.main.utils import get_rest_data, public_route, check_groups +from dashmachine.main.models import Files, Apps, DataSources +from dashmachine.main.utils import ( + get_rest_data, + 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 @@ -72,6 +77,13 @@ def load_rest_data(): 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 + + # ------------------------------------------------------------------------------ # TCDROP routes # ------------------------------------------------------------------------------ diff --git a/dashmachine/main/utils.py b/dashmachine/main/utils.py index 6cb2a45..71a7673 100755 --- a/dashmachine/main/utils.py +++ b/dashmachine/main/utils.py @@ -1,5 +1,6 @@ import os import subprocess +import importlib from shutil import copyfile from requests import get from configparser import ConfigParser @@ -143,3 +144,15 @@ def check_groups(groups, current_user): return True else: return False + + +def get_data_source(data_source): + data_source_args = [] + for arg in data_source.args: + data_source_args.append(row2dict(arg)) + 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/platform/rest.py b/dashmachine/platform/rest.py index ebf0b98..c928fa7 100644 --- a/dashmachine/platform/rest.py +++ b/dashmachine/platform/rest.py @@ -1,6 +1,47 @@ +from requests import get + + class Platform: - def __init__(self, *args, **kwargs): - pass + def __init__(self, data_source, data_source_args): + self.data_source = data_source + self.name = data_source["name"] + + # parse the user's options from the config entries + for source_arg in data_source_args: + if source_arg.get("key") == "resource": + self.resource = source_arg.get("value") + + if source_arg.get("key") == "method": + self.method = source_arg.get("value") + else: + self.method = "GET" + + if source_arg.get("key") == "payload": + self.payload = source_arg.get("value") + + if source_arg.get("key") == "authentication": + self.authentication = source_arg.get("value") + + if source_arg.get("key") == "username": + self.username = source_arg.get("value") + + if source_arg.get("key") == "password": + self.password = source_arg.get("value") + + if source_arg.get("key") == "value_template": + self.value_template = source_arg.get("value") + else: + self.value_template = "value" + + if source_arg.get("key") == "data_template": + self.data_template = source_arg.get("value") + else: + self.value_template = self.name def process(self): - pass + if self.method.upper() == "GET": + try: + value = get(self.resource) + except: + pass + return self.name diff --git a/dashmachine/static/js/main/home.js b/dashmachine/static/js/main/home.js index c4bba42..47f6603 100644 --- a/dashmachine/static/js/main/home.js +++ b/dashmachine/static/js/main/home.js @@ -15,6 +15,19 @@ $( document ).ready(function() { }); }); + $(".data-source-container").each(function(e) { + var el = $(this); + $.ajax({ + url: el.attr('data-url'), + type: 'GET', + data: {id: el.attr('data-id')}, + success: function(data){ + el.closest('.col').find('.data-source-loading').addClass('hide'); + el.text(data); + } + }); + }); + $(".data-template").each(function(e) { var el = $(this); $.ajax({ diff --git a/dashmachine/templates/main/home.html b/dashmachine/templates/main/home.html index 766c392..d36cdd2 100755 --- a/dashmachine/templates/main/home.html +++ b/dashmachine/templates/main/home.html @@ -1,5 +1,5 @@ {% extends "main/layout.html" %} -{% from 'global_macros.html' import data %} +{% from 'global_macros.html' import data, preload_circle %} {% block page_vendor_css %} {% endblock page_vendor_css %} @@ -42,16 +42,20 @@
    - {% if app.data_template %} + {% if app.data_sources.count() > 0 %}
    -

    - {{ app.data_template|safe }} -

    + {{ preload_circle() }} + {% for data_source in app.data_sources %} +

    +

    + {% endfor %}
    {% else %} From 71b5f17f83b7f4f4eebfe59b883d3799fbaf207d Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Thu, 6 Feb 2020 12:00:03 -0500 Subject: [PATCH 15/26] finished the core of platform/plugin system --- dashmachine/main/models.py | 12 --------- dashmachine/main/read_config.py | 3 +-- dashmachine/main/routes.py | 7 ----- dashmachine/main/utils.py | 22 +-------------- dashmachine/platform/rest.py | 21 ++++++++------- dashmachine/static/js/main/home.js | 15 +---------- dashmachine/templates/main/home.html | 2 +- migrations/versions/45ebff47af9f_.py | 40 ++++++++++++++++++++++++++++ 8 files changed, 55 insertions(+), 67 deletions(-) create mode 100644 migrations/versions/45ebff47af9f_.py diff --git a/dashmachine/main/models.py b/dashmachine/main/models.py index 276a920..7ff36a3 100644 --- a/dashmachine/main/models.py +++ b/dashmachine/main/models.py @@ -40,18 +40,6 @@ class TemplateApps(db.Model): open_in = db.Column(db.String()) -class ApiCalls(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()) - - class DataSources(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 index 26e652f..0549d86 100644 --- a/dashmachine/main/read_config.py +++ b/dashmachine/main/read_config.py @@ -1,6 +1,6 @@ import os from configparser import ConfigParser -from dashmachine.main.models import Apps, ApiCalls, Groups, DataSources, DataSourcesArgs +from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs from dashmachine.settings_system.models import Settings from dashmachine.paths import user_data_folder from dashmachine import db @@ -27,7 +27,6 @@ def read_config(): DataSources.query.delete() DataSourcesArgs.query.delete() Apps.query.delete() - ApiCalls.query.delete() Settings.query.delete() Groups.query.delete() diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index 546191a..e92113e 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -6,7 +6,6 @@ from flask import render_template, url_for, redirect, request, Blueprint, jsonif from flask_login import current_user from dashmachine.main.models import Files, Apps, DataSources from dashmachine.main.utils import ( - get_rest_data, public_route, check_groups, get_data_source, @@ -71,12 +70,6 @@ def app_view(app_id): 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() diff --git a/dashmachine/main/utils.py b/dashmachine/main/utils.py index 71a7673..5540c4a 100755 --- a/dashmachine/main/utils.py +++ b/dashmachine/main/utils.py @@ -5,7 +5,7 @@ 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 ApiCalls, TemplateApps, Groups +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 @@ -104,26 +104,6 @@ def dashmachine_init(): 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 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 check_groups(groups, current_user): if current_user.is_anonymous: current_user.role = "public_user" diff --git a/dashmachine/platform/rest.py b/dashmachine/platform/rest.py index c928fa7..a1dc85d 100644 --- a/dashmachine/platform/rest.py +++ b/dashmachine/platform/rest.py @@ -1,4 +1,5 @@ from requests import get +from flask import render_template_string class Platform: @@ -13,8 +14,6 @@ class Platform: if source_arg.get("key") == "method": self.method = source_arg.get("value") - else: - self.method = "GET" if source_arg.get("key") == "payload": self.payload = source_arg.get("value") @@ -30,18 +29,20 @@ class Platform: if source_arg.get("key") == "value_template": self.value_template = source_arg.get("value") - else: - self.value_template = "value" if source_arg.get("key") == "data_template": self.data_template = source_arg.get("value") - else: - self.value_template = self.name + + # set defaults for omitted options + if not hasattr(self, "method"): + self.method = "GET" def process(self): if self.method.upper() == "GET": try: - value = get(self.resource) - except: - pass - return self.name + value = get(self.resource).json() + except Exception as e: + value = f"{e}" + value_template = render_template_string(self.value_template, value=value) + data_template = render_template_string(self.data_template, value=value_template) + return data_template diff --git a/dashmachine/static/js/main/home.js b/dashmachine/static/js/main/home.js index 47f6603..39dc0b9 100644 --- a/dashmachine/static/js/main/home.js +++ b/dashmachine/static/js/main/home.js @@ -23,20 +23,7 @@ $( document ).ready(function() { data: {id: el.attr('data-id')}, success: function(data){ el.closest('.col').find('.data-source-loading').addClass('hide'); - el.text(data); - } - }); - }); - - $(".data-template").each(function(e) { - var el = $(this); - $.ajax({ - url: el.attr('data-url'), - type: 'GET', - data: {template: el.text()}, - success: function(data){ - el.text(data); - el.removeClass('hide'); + el.html(data); } }); }); diff --git a/dashmachine/templates/main/home.html b/dashmachine/templates/main/home.html index d36cdd2..ed3ab05 100755 --- a/dashmachine/templates/main/home.html +++ b/dashmachine/templates/main/home.html @@ -91,4 +91,4 @@ {% block page_lvl_js %} {{ process_js_sources(src="main/home.js")|safe }} -{% endblock page_lvl_js %} +{% endblock page_lvl_js %} \ No newline at end of file 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 ### From 63f9f4b536dc00cc43a3e2e0e9c173c9abdc6f43 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Thu, 6 Feb 2020 21:31:51 -0500 Subject: [PATCH 16/26] starting working on ui for multiple users --- dashmachine/main/routes.py | 4 + dashmachine/settings_system/routes.py | 13 ++- .../static/js/settings_system/settings.js | 15 +++- .../templates/settings_system/settings.html | 49 +---------- .../templates/settings_system/user.html | 88 +++++++++++++++++++ dashmachine/user_system/forms.py | 10 +-- migrations/versions/8f5a046465e8_.py | 40 +++++++++ 7 files changed, 166 insertions(+), 53 deletions(-) create mode 100644 dashmachine/templates/settings_system/user.html create mode 100644 migrations/versions/8f5a046465e8_.py diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index e92113e..811959a 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -64,8 +64,12 @@ def home(): return render_template("main/home.html") +@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}") diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index 9000053..bc63333 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -4,6 +4,7 @@ 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.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 @@ -29,6 +30,7 @@ def settings(): config_form = ConfigForm() user_form = UserForm() + # user_form.role.choices = [(role, role) for role in settings_db.roles.split(",")] 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() @@ -36,6 +38,8 @@ def settings(): 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() return render_template( "settings_system/settings.html", config_form=config_form, @@ -43,6 +47,7 @@ def settings(): user_form=user_form, template_apps=",".join(template_apps), version=version, + users=users, ) @@ -93,7 +98,13 @@ 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) + if not form.id.data: + new = True + else: + new = False + add_edit_user( + form.username.data, form.password.data, user_id=form.id.data, new=new + ) else: err_str = "" for fieldName, errorMessages in form.errors.items(): diff --git a/dashmachine/static/js/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index 3ce6514..df6849b 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -4,6 +4,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({ @@ -58,7 +63,7 @@ $( 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', @@ -75,4 +80,12 @@ $( document ).ready(function() { }); }); + $(".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(); + }); + }); \ No newline at end of file diff --git a/dashmachine/templates/settings_system/settings.html b/dashmachine/templates/settings_system/settings.html index 01e9fd4..b213979 100644 --- a/dashmachine/templates/settings_system/settings.html +++ b/dashmachine/templates/settings_system/settings.html @@ -1,7 +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/config-readme.html' import ConfigReadme %} +{% from 'settings_system/user.html' import UserTab with context %} {% block page_vendor_css %} {% endblock page_vendor_css %} @@ -133,51 +134,7 @@
    -
    -
    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..820070a --- /dev/null +++ b/dashmachine/templates/settings_system/user.html @@ -0,0 +1,88 @@ +{% macro UserTab() %} + + +
    +
    +
    Users + + add + +
    + {% for user in users %} +
    +
    + + {{ user.username }} + {{ user.role }} + + + edit + close + +
    +
    + {% endfor %} +
    +
    + + +
    +
    DashMachine
    +

    version: {{ version }}

    +
    +{% endmacro %} \ No newline at end of file diff --git a/dashmachine/user_system/forms.py b/dashmachine/user_system/forms.py index 7372c67..02e9fe1 100644 --- a/dashmachine/user_system/forms.py +++ b/dashmachine/user_system/forms.py @@ -1,9 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import ( - StringField, - PasswordField, - BooleanField, -) +from wtforms import StringField, PasswordField, BooleanField, SelectField from wtforms.validators import DataRequired, Length @@ -12,6 +8,10 @@ class UserForm(FlaskForm): password = PasswordField(validators=[DataRequired(), Length(min=8, max=120)]) + # role = SelectField() + + id = StringField() + confirm_password = PasswordField() remember = BooleanField() 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 ### From dba238fa29175e9efff5100accc784454440304b Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Fri, 7 Feb 2020 08:43:51 -0500 Subject: [PATCH 17/26] tweaking plugin/platform system, added ping platform --- dashmachine/main/utils.py | 5 +- dashmachine/platform/ping.py | 23 ++++++++ dashmachine/platform/rest.py | 56 ++++++++----------- .../static/css/global/dashmachine-theme.css | 16 +++++- .../static/js/settings_system/settings.js | 4 +- dashmachine/templates/main/tcdrop.html | 4 +- 6 files changed, 68 insertions(+), 40 deletions(-) create mode 100644 dashmachine/platform/ping.py diff --git a/dashmachine/main/utils.py b/dashmachine/main/utils.py index 5540c4a..3727022 100755 --- a/dashmachine/main/utils.py +++ b/dashmachine/main/utils.py @@ -127,9 +127,10 @@ def check_groups(groups, current_user): def get_data_source(data_source): - data_source_args = [] + data_source_args = {} for arg in data_source.args: - data_source_args.append(row2dict(arg)) + 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']}", "." diff --git a/dashmachine/platform/ping.py b/dashmachine/platform/ping.py new file mode 100644 index 0000000..06dd47a --- /dev/null +++ b/dashmachine/platform/ping.py @@ -0,0 +1,23 @@ +from requests import get + + +class Platform: + def __init__(self, data_source, data_source_args): + # parse the user's options from the config entries + for key, value in data_source_args.items(): + self.__dict__[key] = value + + def process(self): + try: + value = get(self.resource) + except Exception: + icon_class = "theme-failure-text" + + if 599 >= value.status_code >= 400: + icon_class = "theme-failure-text" + if 399 >= value.status_code >= 300: + icon_class = "theme-warning-text" + if 299 >= value.status_code >= 100: + icon_class = "theme-success-text" + + return f"fiber_manual_record " diff --git a/dashmachine/platform/rest.py b/dashmachine/platform/rest.py index a1dc85d..455a863 100644 --- a/dashmachine/platform/rest.py +++ b/dashmachine/platform/rest.py @@ -1,41 +1,20 @@ -from requests import get +import json +from requests import get, post +from requests.auth import HTTPBasicAuth, HTTPDigestAuth from flask import render_template_string class Platform: def __init__(self, data_source, data_source_args): - self.data_source = data_source - self.name = data_source["name"] - # parse the user's options from the config entries - for source_arg in data_source_args: - if source_arg.get("key") == "resource": - self.resource = source_arg.get("value") + for key, value in data_source_args.items(): + self.__dict__[key] = value - if source_arg.get("key") == "method": - self.method = source_arg.get("value") - - if source_arg.get("key") == "payload": - self.payload = source_arg.get("value") - - if source_arg.get("key") == "authentication": - self.authentication = source_arg.get("value") - - if source_arg.get("key") == "username": - self.username = source_arg.get("value") - - if source_arg.get("key") == "password": - self.password = source_arg.get("value") - - if source_arg.get("key") == "value_template": - self.value_template = source_arg.get("value") - - if source_arg.get("key") == "data_template": - self.data_template = source_arg.get("value") - - # set defaults for omitted options - if not hasattr(self, "method"): - self.method = "GET" + # 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": @@ -43,6 +22,17 @@ class Platform: 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) - data_template = render_template_string(self.data_template, value=value_template) - return data_template + return value_template 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/js/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index df6849b..dcfc156 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -20,7 +20,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"}); } } }); @@ -70,7 +70,7 @@ $( document ).ready(function() { 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(''); diff --git a/dashmachine/templates/main/tcdrop.html b/dashmachine/templates/main/tcdrop.html index 857c364..5bd918a 100644 --- a/dashmachine/templates/main/tcdrop.html +++ b/dashmachine/templates/main/tcdrop.html @@ -16,8 +16,8 @@ {{ preload_circle() }}
  • - error - + error +
  • From 3bda8cc3e83892f42e74dd1c1205414b049a07cd Mon Sep 17 00:00:00 2001 From: ShadeAnimator Date: Fri, 7 Feb 2020 18:18:17 +0300 Subject: [PATCH 18/26] Transmission platform added --- dashmachine/main/utils.py | 2 +- dashmachine/platform/ping.py | 4 ++-- dashmachine/platform/rest.py | 4 ++-- dashmachine/platform/transmission.py | 34 ++++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 dashmachine/platform/transmission.py diff --git a/dashmachine/main/utils.py b/dashmachine/main/utils.py index 3727022..b4d23ba 100755 --- a/dashmachine/main/utils.py +++ b/dashmachine/main/utils.py @@ -135,5 +135,5 @@ def get_data_source(data_source): module = importlib.import_module( f"dashmachine.platform.{data_source['platform']}", "." ) - platform = module.Platform(data_source, data_source_args) + platform = module.Platform(data_source, **data_source_args) return platform.process() diff --git a/dashmachine/platform/ping.py b/dashmachine/platform/ping.py index 06dd47a..9f7ed1b 100644 --- a/dashmachine/platform/ping.py +++ b/dashmachine/platform/ping.py @@ -2,9 +2,9 @@ from requests import get class Platform: - def __init__(self, data_source, data_source_args): + def __init__(self, *args, **kwargs): # parse the user's options from the config entries - for key, value in data_source_args.items(): + for key, value in kwargs.items(): self.__dict__[key] = value def process(self): diff --git a/dashmachine/platform/rest.py b/dashmachine/platform/rest.py index 455a863..61b5d61 100644 --- a/dashmachine/platform/rest.py +++ b/dashmachine/platform/rest.py @@ -5,9 +5,9 @@ from flask import render_template_string class Platform: - def __init__(self, data_source, data_source_args): + def __init__(self, *args, **kwargs): # parse the user's options from the config entries - for key, value in data_source_args.items(): + for key, value in kwargs.items(): self.__dict__[key] = value # set defaults for omitted options diff --git a/dashmachine/platform/transmission.py b/dashmachine/platform/transmission.py new file mode 100644 index 0000000..ac6e70f --- /dev/null +++ b/dashmachine/platform/transmission.py @@ -0,0 +1,34 @@ +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/requirements.txt b/requirements.txt index 2033357..0474a60 100755 --- a/requirements.txt +++ b/requirements.txt @@ -32,3 +32,4 @@ SQLAlchemy==1.3.13 urllib3==1.25.8 Werkzeug==0.16.1 WTForms==2.2.1 +transmissionrpc \ No newline at end of file From f697aae79639390d8f3c0618029ece230d7a14e9 Mon Sep 17 00:00:00 2001 From: Nixellion Date: Fri, 7 Feb 2020 21:21:27 +0300 Subject: [PATCH 19/26] Pihole platform --- dashmachine/platform/pihole.py | 145 +++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 dashmachine/platform/pihole.py diff --git a/dashmachine/platform/pihole.py b/dashmachine/platform/pihole.py new file mode 100644 index 0000000..2f42999 --- /dev/null +++ b/dashmachine/platform/pihole.py @@ -0,0 +1,145 @@ +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 + + From da44d56fa8e521acd3efbf638f04da928f3c394c Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Fri, 7 Feb 2020 13:46:44 -0500 Subject: [PATCH 20/26] updated ping.py --- dashmachine/platform/ping.py | 18 ++++++++---------- dashmachine/platform/transmission.py | 10 +++++++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/dashmachine/platform/ping.py b/dashmachine/platform/ping.py index 9f7ed1b..c17b2fb 100644 --- a/dashmachine/platform/ping.py +++ b/dashmachine/platform/ping.py @@ -1,4 +1,5 @@ -from requests import get +import platform +import subprocess class Platform: @@ -8,16 +9,13 @@ class Platform: self.__dict__[key] = value def process(self): - try: - value = get(self.resource) - except Exception: - icon_class = "theme-failure-text" + param = "-n" if platform.system().lower() == "windows" else "-c" + command = ["ping", param, "1", self.resource] + up = subprocess.call(command) == 0 - if 599 >= value.status_code >= 400: - icon_class = "theme-failure-text" - if 399 >= value.status_code >= 300: - icon_class = "theme-warning-text" - if 299 >= value.status_code >= 100: + 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/transmission.py b/dashmachine/platform/transmission.py index ac6e70f..0fef512 100644 --- a/dashmachine/platform/transmission.py +++ b/dashmachine/platform/transmission.py @@ -6,6 +6,7 @@ import transmissionrpc # from pprint import PrettyPrinter # pp = PrettyPrinter() + class Platform: def __init__(self, *args, **kwargs): # parse the user's options from the config entries @@ -15,20 +16,23 @@ class Platform: if not hasattr(self, "port"): self.port = 9091 if not hasattr(self, "host"): - self.host = 'localhost' + self.host = "localhost" - self.tc = transmissionrpc.Client(self.host, port=self.port, user=self.user, password=self.password) + 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(): + 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() From 071d12a28505c200b3877c3dc1a16c02e7853051 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Sat, 8 Feb 2020 09:12:40 -0500 Subject: [PATCH 21/26] working on user system --- dashmachine/user_system/utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dashmachine/user_system/utils.py b/dashmachine/user_system/utils.py index e01bf9e..b641955 100755 --- a/dashmachine/user_system/utils.py +++ b/dashmachine/user_system/utils.py @@ -2,14 +2,12 @@ from dashmachine import db, bcrypt from dashmachine.user_system.models import User -def add_edit_user(username, password, user_id=None, role=None, new=False): +def add_edit_user(username, password, user_id=None, role=None): if user_id: user = User.query.filter_by(id=user_id).first() - elif new: - user = User() + if not user: + user = User() else: - user = User.query.first() - if not user: user = User() hashed_password = bcrypt.generate_password_hash(password).decode("utf-8") From 2d0f53ca4fe75d87748577568409c70ae2d438bb Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Sat, 8 Feb 2020 09:12:57 -0500 Subject: [PATCH 22/26] working on user system --- config_readme.md | 0 dashmachine/platform/pihole.py | 115 +++++++++++++----- dashmachine/settings_system/routes.py | 10 +- .../templates/settings_system/user.html | 15 +-- dashmachine/user_system/forms.py | 11 +- dashmachine/user_system/routes.py | 6 +- requirements.txt | 3 +- 7 files changed, 113 insertions(+), 47 deletions(-) create mode 100644 config_readme.md diff --git a/config_readme.md b/config_readme.md new file mode 100644 index 0000000..e69de29 diff --git a/dashmachine/platform/pihole.py b/dashmachine/platform/pihole.py index 2f42999..b4d24e0 100644 --- a/dashmachine/platform/pihole.py +++ b/dashmachine/platform/pihole.py @@ -13,7 +13,9 @@ def inApiLink(ip, endpoint): 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.token = hashlib.sha256( + hashlib.sha256(str(password).encode()).hexdigest().encode() + ).hexdigest() self.auth_timestamp = time.time() @@ -26,20 +28,33 @@ class PiHole(object): self.pw = None def refresh(self): - rawdata = requests.get("http://" + self.ip_address + "/admin/api.php?summary").json() + 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() + "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() + "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"] + "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"] @@ -60,14 +75,25 @@ class PiHole(object): 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() + 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"]} + 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) @@ -79,54 +105,85 @@ class PiHole(object): 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"] + 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) + 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) + "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() + 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"]) + 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() + 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 + 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 + 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: @@ -139,7 +196,7 @@ class Platform: def process(self): self.pihole.refresh() - value_template = render_template_string(self.value_template, **self.pihole.__dict__) + value_template = render_template_string( + self.value_template, **self.pihole.__dict__ + ) return value_template - - diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index bc63333..1acc0c4 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -30,7 +30,6 @@ def settings(): config_form = ConfigForm() user_form = UserForm() - # user_form.role.choices = [(role, role) for role in settings_db.roles.split(",")] 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() @@ -98,12 +97,11 @@ def edit_user(): if form.validate_on_submit(): if form.password.data != form.confirm_password.data: return jsonify(data={"err": "Passwords don't match"}) - if not form.id.data: - new = True - else: - new = False add_edit_user( - form.username.data, form.password.data, user_id=form.id.data, new=new + form.username.data, + form.password.data, + user_id=form.id.data, + role=form.role.data, ) else: err_str = "" diff --git a/dashmachine/templates/settings_system/user.html b/dashmachine/templates/settings_system/user.html index 820070a..561bd23 100644 --- a/dashmachine/templates/settings_system/user.html +++ b/dashmachine/templates/settings_system/user.html @@ -6,12 +6,12 @@
    {{ user_form.hidden_tag() }} -{# {{ select(#} -{# id='user-form-role',#} -{# form_obj=user_form.role,#} -{# size="s12",#} -{# label='Role'#} -{# ) }}#} + {{ select( + id='user-form-role', + form_obj=user_form.role, + size="s12", + label='Role' + ) }} {{ input( label="Username", @@ -85,4 +85,5 @@
    DashMachine

    version: {{ version }}

    -{% endmacro %} \ No newline at end of file +{% endmacro %} +{{UserTab()}} \ No newline at end of file diff --git a/dashmachine/user_system/forms.py b/dashmachine/user_system/forms.py index 02e9fe1..e36ca0b 100644 --- a/dashmachine/user_system/forms.py +++ b/dashmachine/user_system/forms.py @@ -1,6 +1,9 @@ from flask_wtf import FlaskForm 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): @@ -8,10 +11,16 @@ class UserForm(FlaskForm): password = PasswordField(validators=[DataRequired(), Length(min=8, max=120)]) - # role = SelectField() + 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/routes.py b/dashmachine/user_system/routes.py index 6ee494a..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,7 +18,7 @@ user_system = Blueprint("user_system", __name__) def login(): user = User.query.first() - form = UserForm() + form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data.lower()).first() diff --git a/requirements.txt b/requirements.txt index 0474a60..99c7509 100755 --- a/requirements.txt +++ b/requirements.txt @@ -32,4 +32,5 @@ SQLAlchemy==1.3.13 urllib3==1.25.8 Werkzeug==0.16.1 WTForms==2.2.1 -transmissionrpc \ No newline at end of file +transmissionrpc +markdown2 \ No newline at end of file From fb621ae66dd2552fffae1ff135f4ea64796b2ce6 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Sat, 8 Feb 2020 11:07:31 -0500 Subject: [PATCH 23/26] new readme system --- config_readme.md | 121 ++++++++ dashmachine/settings_system/routes.py | 7 + .../static/css/settings_system/settings.css | 36 +++ .../static/js/settings_system/settings.js | 1 - .../settings_system/config-readme.html | 279 ------------------ .../templates/settings_system/settings.html | 23 +- 6 files changed, 174 insertions(+), 293 deletions(-) delete mode 100644 dashmachine/templates/settings_system/config-readme.html diff --git a/config_readme.md b/config_readme.md index e69de29..d5deaa8 100644 --- a/config_readme.md +++ b/config_readme.md @@ -0,0 +1,121 @@ +#### Config.ini Readme + +##### Settings +```ini +[Settings] +theme = dark +accent = orange +background = static/images/backgrounds/background.png +``` + +| 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 +```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 +``` + +| 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 | + +##### Access Groups +```ini +[public] +roles = admin, user, public_user +``` + +| 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` + +##### ping +```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 | + +##### rest +```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 +>``` \ No newline at end of file diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index 1acc0c4..91ee999 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -1,5 +1,6 @@ import os from shutil import move +from markdown2 import markdown_path from flask_login import current_user from flask import render_template, request, Blueprint, jsonify, redirect, url_for from dashmachine.user_system.forms import UserForm @@ -15,6 +16,7 @@ from dashmachine.paths import ( backgrounds_images_folder, icons_images_folder, user_data_folder, + root_folder, ) from dashmachine.version import version @@ -39,6 +41,10 @@ def settings(): template_apps.append(f"{t_app.name}&&{t_app.icon}") users = User.query.all() + config_readme = markdown_path( + os.path.join(root_folder, "config_readme.md"), + extras=["tables", "fenced-code-blocks", "break-on-newline", "header-ids"], + ) return render_template( "settings_system/settings.html", config_form=config_form, @@ -47,6 +53,7 @@ def settings(): template_apps=",".join(template_apps), version=version, users=users, + config_readme=config_readme, ) 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/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index dcfc156..8157684 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -3,7 +3,6 @@ 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'); diff --git a/dashmachine/templates/settings_system/config-readme.html b/dashmachine/templates/settings_system/config-readme.html deleted file mode 100644 index 04f431d..0000000 --- a/dashmachine/templates/settings_system/config-readme.html +++ /dev/null @@ -1,279 +0,0 @@ -{% macro ConfigReadme() %} -
    -

    Config.ini Readme - close -

    -
    -
    -
    Settings
    - - [Settings]
    - theme = dark
    - accent = orange
    - background = static/images/backgrounds/background.png
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    VariableRequiredDescriptionOptions
    [Settings]YesConfig section name.string
    themeYesUI themelight, dark
    accentYesUI accent color - orange, red, pink, purple, deepPurple, indigo, blue, lightBlue, - cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey -
    backgroundYesBackground image for the UI/static/images/backgrounds/yourpicture.png, external link to image, None, random
    rolesNoUser roles for access groups.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_groupsNoDefine which access groups can access the /home pageRoles defined in your config. If not defined, default is admin_only
    settings_access_groupsNoDefine which access groups can access the /settings pageRoles defined in your config. If not defined, default is admin_only
    - -
    Apps
    - - [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_template = None -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    VariableRequiredDescriptionOptions
    [App Name]YesThe name of your app.string
    prefixYesThe prefix for the app's url.web prefix, e.g. http:// or https://
    urlYesThe url for your app.web url, e.g. myapp.com
    iconNoIcon for the dashboard./static/images/icons/yourpicture.png, external link to image
    sidebar_iconNoIcon for the sidenav./static/images/icons/yourpicture.png, external link to image
    descriptionNoA short description for the app.string
    open_inYesopen the app in the current tab, an iframe or a new tabiframe, new_tab, this_tab
    data_templateNoTemplate for displaying variable(s) from rest data *Note: you must have a rest data variable set up in the configexample: Data: {{ '{{ your_variable }}' }}
    - -
    Access Groups
    - - - [public]
    - roles = admin, user, public_user
    -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    VariableRequiredDescriptionOptions
    [Group Name]YesName for access groupstring
    rolesYesA comma separated list of user roles allowed to view apps in this access groupRoles defined in your config. If not defined, defaults are admin and 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' - - -
    Api Data
    - - [variable_name]
    - platform = rest
    - resource = your-website.com
    - value_template = variable_name
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    VariableRequiredDescriptionOptions
    [variable_name]YesThe variable to be made available to apps.variable (python syntax)
    platformYesPlatform for data sourcerest
    resourceYesThe url for the api call.myapp.com/api/hello
    value_templateNoTranform the data returned by the api call (python syntax)variable_name[0]['info']
    methodNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
    payloadNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
    authenticationNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
    usernameNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
    passwordNOT IMPLEMENTEDNOT IMPLEMENTEDNOT IMPLEMENTED
    - -
    Api Data Example
    -

    Say we wanted to display how many Pokemon there are using the PokeAPI, we would add the following to the config:

    - - [num_pokemon]
    - platform = rest
    - resource = https://pokeapi.co/api/v2/pokemon
    - value_template = num_pokemon['count']
    -
    - -

    Then in the config entry for the app you want to add this to, you would add:

    - - - data_template = Pokemon: {{ '{{ num_pokemon }}' }} - - -
    -{% endmacro %} \ No newline at end of file diff --git a/dashmachine/templates/settings_system/settings.html b/dashmachine/templates/settings_system/settings.html index b213979..29b253a 100644 --- a/dashmachine/templates/settings_system/settings.html +++ b/dashmachine/templates/settings_system/settings.html @@ -1,7 +1,6 @@ {% extends "main/layout.html" %} {% from 'global_macros.html' import input, button, select %} {% from 'main/tcdrop.html' import tcdrop %} -{% from 'settings_system/config-readme.html' import ConfigReadme %} {% from 'settings_system/user.html' import UserTab with context %} {% block page_vendor_css %} {% endblock page_vendor_css %} @@ -21,13 +20,6 @@ {% endblock page_lvl_css %} {% block content %} -
    @@ -37,11 +29,7 @@
    -
    Config - - info - -
    +
    Config.ini
    {{ button( icon="save", id="save-config-btn", @@ -73,6 +61,10 @@
    + +
    + +
    + {{ config_readme|safe }}
    From 25c530e5fce0e0a995f8ac41f491e6d6d9dbb65f Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Sat, 8 Feb 2020 11:22:20 -0500 Subject: [PATCH 24/26] update config_readme.md --- config_readme.md | 15 +++++++++++++++ dashmachine/settings_system/routes.py | 8 +++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/config_readme.md b/config_readme.md index d5deaa8..cc3ec98 100644 --- a/config_readme.md +++ b/config_readme.md @@ -1,6 +1,11 @@ #### 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 = dark @@ -19,6 +24,8 @@ background = static/images/backgrounds/background.png | 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:// @@ -42,11 +49,17 @@ data_sources = None | 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 | ##### 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] | @@ -65,6 +78,7 @@ permanent, submit a pull request for it to be added by default! `data_source = variable_name` ##### ping +Check if a service is online. ```ini [variable_name] platform = ping @@ -79,6 +93,7 @@ resource = 192.168.1.1 | resource | Yes | Url of whatever you want to ping | url | ##### rest +Make a call on a REST API and display the results as a jinja formatted string. ```ini [variable_name] platform = rest diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index 91ee999..3c5dbd6 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -43,7 +43,13 @@ def settings(): users = User.query.all() config_readme = markdown_path( os.path.join(root_folder, "config_readme.md"), - extras=["tables", "fenced-code-blocks", "break-on-newline", "header-ids"], + extras=[ + "tables", + "fenced-code-blocks", + "break-on-newline", + "header-ids", + "code-friendly", + ], ) return render_template( "settings_system/settings.html", From 91c5350330eab6d72d1235e902fb39202cdb82a5 Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Sat, 8 Feb 2020 12:46:44 -0500 Subject: [PATCH 25/26] move platform readmes to the their modules --- config_readme.md | 60 +------------------ dashmachine/paths.py | 2 + dashmachine/platform/ping.py | 20 +++++++ dashmachine/platform/rest.py | 47 +++++++++++++++ dashmachine/settings_system/routes.py | 19 ++---- dashmachine/settings_system/utils.py | 40 +++++++++++-- .../static/js/settings_system/settings.js | 7 ++- .../templates/settings_system/user.html | 45 ++++++++------ 8 files changed, 142 insertions(+), 98 deletions(-) diff --git a/config_readme.md b/config_readme.md index cc3ec98..817436e 100644 --- a/config_readme.md +++ b/config_readme.md @@ -75,62 +75,4 @@ 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` - -##### 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 | - -##### 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 ->``` \ No newline at end of file +`data_source = variable_name` \ No newline at end of file diff --git a/dashmachine/paths.py b/dashmachine/paths.py index d5e6bb0..800487e 100755 --- a/dashmachine/paths.py +++ b/dashmachine/paths.py @@ -13,6 +13,8 @@ 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") diff --git a/dashmachine/platform/ping.py b/dashmachine/platform/ping.py index c17b2fb..f182e5c 100644 --- a/dashmachine/platform/ping.py +++ b/dashmachine/platform/ping.py @@ -1,3 +1,23 @@ +""" + +##### 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 diff --git a/dashmachine/platform/rest.py b/dashmachine/platform/rest.py index 61b5d61..ef8825c 100644 --- a/dashmachine/platform/rest.py +++ b/dashmachine/platform/rest.py @@ -1,3 +1,50 @@ +""" + +##### 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 diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index 3c5dbd6..aa5b0ef 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -1,6 +1,5 @@ import os from shutil import move -from markdown2 import markdown_path from flask_login import current_user from flask import render_template, request, Blueprint, jsonify, redirect, url_for from dashmachine.user_system.forms import UserForm @@ -10,13 +9,12 @@ 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.settings_system.forms import ConfigForm -from dashmachine.settings_system.utils import load_files_html +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, - root_folder, ) from dashmachine.version import version @@ -41,16 +39,7 @@ def settings(): template_apps.append(f"{t_app.name}&&{t_app.icon}") users = User.query.all() - config_readme = markdown_path( - os.path.join(root_folder, "config_readme.md"), - extras=[ - "tables", - "fenced-code-blocks", - "break-on-newline", - "header-ids", - "code-friendly", - ], - ) + config_readme = get_config_html() return render_template( "settings_system/settings.html", config_form=config_form, @@ -123,4 +112,6 @@ 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}) 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/static/js/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index 8157684..1204493 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -71,9 +71,10 @@ $( document ).ready(function() { if (data.data.err !== 'success'){ 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); + $("#edit-user-modal").modal('close'); + M.toast({html: 'User saved'}); } } }); diff --git a/dashmachine/templates/settings_system/user.html b/dashmachine/templates/settings_system/user.html index 561bd23..f4097a3 100644 --- a/dashmachine/templates/settings_system/user.html +++ b/dashmachine/templates/settings_system/user.html @@ -60,23 +60,11 @@ add - {% for user in users %} -
    -
    - - {{ user.username }} - {{ user.role }} - - - edit - close - -
    -
    - {% endfor %} + +
    + {{ Users(users) }} +
    +
    @@ -86,4 +74,25 @@

    version: {{ version }}

    {% endmacro %} -{{UserTab()}} \ No newline at end of file + +{% macro Users(users) %} + {% for user in users %} +
    +
    + + {{ user.username }} + {{ user.role }} + + + edit + close + +
    +
    + {% endfor %} +{% endmacro %} + +{{Users(users)}} \ No newline at end of file From 838831b857090dac6f6ed06d9bd66c660d5c143d Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Sat, 8 Feb 2020 17:05:48 -0500 Subject: [PATCH 26/26] 0.3 ready for docker testing --- README.md | 17 +- config_readme.md | 11 +- dashmachine/error_pages/routes.py | 5 + dashmachine/main/forms.py | 6 + dashmachine/main/models.py | 6 + dashmachine/main/read_config.py | 14 +- dashmachine/main/routes.py | 11 +- dashmachine/settings_system/routes.py | 19 ++- dashmachine/static/css/global/dashmachine.css | 3 + dashmachine/static/css/main/home.css | 23 +++ dashmachine/static/js/main/home.js | 11 ++ .../static/js/settings_system/settings.js | 38 ++--- .../templates/error_pages/unauthorized.html | 11 ++ dashmachine/templates/global_macros.html | 9 +- dashmachine/templates/main/home.html | 17 +- dashmachine/templates/main/layout.html | 15 +- .../templates/settings_system/files.html | 154 +++++++++++------- .../templates/settings_system/settings.html | 24 +-- .../templates/settings_system/user.html | 36 +++- dashmachine/user_system/utils.py | 4 + dashmachine/version.py | 2 +- default_config.ini | 5 +- migrations/versions/885c5f9b33d5_.py | 28 ++++ 23 files changed, 325 insertions(+), 144 deletions(-) create mode 100644 dashmachine/static/css/main/home.css create mode 100644 dashmachine/templates/error_pages/unauthorized.html create mode 100644 migrations/versions/885c5f9b33d5_.py 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 index 817436e..8c30c7c 100644 --- a/config_readme.md +++ b/config_readme.md @@ -8,9 +8,12 @@ If you change the config.ini file, you either have to restart the container config to be applied. ```ini [Settings] -theme = dark +theme = light accent = orange -background = static/images/backgrounds/background.png +background = None +roles = admin,user,public_user +home_access_groups = admin_only +settings_access_groups = admin_only ``` | Variable | Required | Description | Options | @@ -35,6 +38,8 @@ 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 | @@ -47,6 +52,8 @@ data_sources = None | 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 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 7ff36a3..9637d1e 100644 --- a/dashmachine/main/models.py +++ b/dashmachine/main/models.py @@ -27,6 +27,7 @@ class Apps(db.Model): 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): @@ -63,3 +64,8 @@ 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 index 0549d86..040e145 100644 --- a/dashmachine/main/read_config.py +++ b/dashmachine/main/read_config.py @@ -1,6 +1,6 @@ import os from configparser import ConfigParser -from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs +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 @@ -29,6 +29,7 @@ def read_config(): Apps.query.delete() Settings.query.delete() Groups.query.delete() + Tags.query.delete() for section in config.sections(): @@ -139,6 +140,17 @@ def read_config(): 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() diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index 811959a..fbb4e0f 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -4,7 +4,8 @@ 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, Apps, DataSources +from dashmachine.main.models import Files, Apps, DataSources, Tags +from dashmachine.main.forms import TagsForm from dashmachine.main.utils import ( public_route, check_groups, @@ -58,10 +59,14 @@ def check_valid_login(): @main.route("/") @main.route("/home", methods=["GET", "POST"]) def home(): + 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("user_system.login")) - return render_template("main/home.html") + return redirect(url_for("error_pages.unauthorized")) + return render_template("main/home.html", tags_form=tags_form) @public_route diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index aa5b0ef..277f25d 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -17,6 +17,7 @@ from dashmachine.paths import ( user_data_folder, ) from dashmachine.version import version +from dashmachine import db settings_system = Blueprint("settings_system", __name__) @@ -99,12 +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( + 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(): @@ -115,3 +118,17 @@ def edit_user(): 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/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/js/main/home.js b/dashmachine/static/js/main/home.js index 39dc0b9..664916b 100644 --- a/dashmachine/static/js/main/home.js +++ b/dashmachine/static/js/main/home.js @@ -27,4 +27,15 @@ $( document ).ready(function() { } }); }); + + $("#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'); + } + }); + }); }); \ No newline at end of file diff --git a/dashmachine/static/js/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index 1204493..99950f8 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -63,29 +63,21 @@ $( document ).ready(function() { }); $("#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); - $("#edit-user-modal").modal('close'); - M.toast({html: 'User saved'}); - } - } - }); - }); - - $(".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(); + $.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 ed3ab05..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, preload_circle %} +{% 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 29b253a..2990c23 100644 --- a/dashmachine/templates/settings_system/settings.html +++ b/dashmachine/templates/settings_system/settings.html @@ -2,6 +2,7 @@ {% 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 %} @@ -80,28 +81,7 @@
    -
    -
    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() }}
    diff --git a/dashmachine/templates/settings_system/user.html b/dashmachine/templates/settings_system/user.html index f4097a3..99992e2 100644 --- a/dashmachine/templates/settings_system/user.html +++ b/dashmachine/templates/settings_system/user.html @@ -61,7 +61,7 @@ -
    +
    {{ Users(users) }}
    @@ -88,11 +88,43 @@ data-role="{{ user.role }}" data-id="{{ user.id }}" data-username="{{ user.username }}">edit
    - close + close
    {% endfor %} + {% endmacro %} {{Users(users)}} \ No newline at end of file diff --git a/dashmachine/user_system/utils.py b/dashmachine/user_system/utils.py index b641955..4855319 100755 --- a/dashmachine/user_system/utils.py +++ b/dashmachine/user_system/utils.py @@ -10,6 +10,10 @@ def add_edit_user(username, password, user_id=None, role=None): 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 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/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 ###