auth 99% ok missing frontend style

This commit is contained in:
jr-k 2024-05-09 21:59:42 +02:00
parent 885c30ded3
commit a9c899f410
27 changed files with 700 additions and 39 deletions

View File

@ -1,3 +1,4 @@
DEBUG=false
PORT=5000
SECRET_KEY=ANY_SECRET_KEY_HERE
AUTOCONFIGURE_LX_FILE=/home/pi/.config/lxsession/LXDE-pi/autostart # Replace by "./var/run/dummy" if not needed

106
data/www/js/auth.js Normal file
View File

@ -0,0 +1,106 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-users');
const $tableInactive = $('table.inactive-users');
const $modalsRoot = $('.modals');
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
};
const updateTable = function () {
$('table').each(function () {
if ($(this).find('tbody tr.user-item:visible').length === 0) {
$(this).find('tr.empty-tr').removeClass('hidden');
} else {
$(this).find('tr.empty-tr').addClass('hidden');
}
});
}
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const main = function () {
};
$(document).on('change', 'input[type=checkbox]', function () {
$.ajax({
url: '/auth/user/toggle',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this)), enabled: $(this).is(':checked')}),
method: 'POST',
});
const $tr = $(this).parents('tr:eq(0)').remove().clone();
if ($(this).is(':checked')) {
$tableActive.append($tr);
} else {
$tableInactive.append($tr);
}
updateTable();
});
$(document).on('change', '#user-add-type', function () {
const value = $(this).val();
const inputType = $(this).find('option').filter(function (i, el) {
return $(el).val() === value;
}).data('input');
$('.user-add-object-input')
.addClass('hidden')
.prop('disabled', true)
.filter('#user-add-object-input-' + inputType)
.removeClass('hidden')
.prop('disabled', false)
;
});
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.user-add', function () {
showModal('modal-user-add');
$('.modal-user-add input:eq(0)').focus().select();
});
$(document).on('click', '.user-edit', function () {
const user = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-user-edit');
$('.modal-user-edit input:visible:eq(0)').focus().select();
$('#user-edit-username').val(user.username);
$('#user-edit-id').val(user.id);
});
$(document).on('click', '.user-delete', function () {
if (confirm(l.auth_user_delete_confirmation)) {
const $tr = $(this).parents('tr:eq(0)');
$tr.remove();
updateTable();
$.ajax({
method: 'DELETE',
url: '/auth/user/delete',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this))}),
});
}
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -55,6 +55,24 @@
"fleet_screen_form_button_cancel": "Cancel",
"js_fleet_screen_delete_confirmation": "Are you sure?",
"login_page_title": "Login",
"auth_page_title": "Users",
"auth_user_button_add": "Add a user",
"auth_user_panel_active": "Active users",
"auth_user_panel_inactive": "Inactive users",
"auth_user_panel_empty": "Currently, there are no users. %link% now.",
"auth_user_panel_th_username": "Username",
"auth_user_panel_th_enabled": "Enabled",
"auth_user_panel_th_activity": "Options",
"auth_user_form_add_title": "Add User",
"auth_user_form_add_submit": "Add",
"auth_user_form_edit_title": "Edit User",
"auth_user_form_edit_submit": "Save",
"auth_user_form_label_username": "Username",
"auth_user_form_label_password": "Password",
"auth_user_form_button_cancel": "Cancel",
"js_auth_user_delete_confirmation": "Are you sure?",
"settings_page_title": "Settings",
"settings_variable_panel_system_variables": "General settings",
"settings_variable_panel_plugin_variables": "Plugins settings",
@ -68,6 +86,7 @@
"settings_variable_form_button_cancel": "Cancel",
"settings_variable_desc_lang": "Server language",
"settings_variable_desc_fleet_enabled": "Enable fleet screen management view",
"settings_variable_desc_auth_enabled": "Enable auth management",
"settings_variable_desc_external_url": "External url (i.e: https://screen-01.company.com or http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Slide upload limit (in bytes, 32*1024*1024 for 32MB)",
"settings_variable_desc_default_slide_duration": "Intro slide duration (in seconds)",
@ -115,15 +134,16 @@
"common_unknown_ipaddr": "Unknown IP address",
"common_empty": "[Empty]",
"logout": "Logout",
"enum_animation_speed_slower": "Slower",
"enum_animation_speed_slow": "Slow",
"enum_animation_speed_normal": "Normal",
"enum_animation_speed_fast": "Fast",
"enum_animation_speed_faster": "Faster",
"enum_variable_section_general": "General",
"enum_variable_section_player_animation": "Player animation",
"enum_variable_section_player_options": "Options du lecteur",
"enum_variable_section_general": "1. General",
"enum_variable_section_player_options": "2. Options du lecteur",
"enum_variable_section_player_animation": "3. Player animation",
"enum_application_language_english": "English",
"enum_application_language_french": "French",
"enum_slide_type_url": "URL",

View File

@ -55,6 +55,24 @@
"fleet_screen_form_button_cancel": "Annuler",
"js_fleet_screen_delete_confirmation": "Êtes-vous sûr ?",
"login_page_title": "Connexion",
"auth_page_title": "Utilisateurs",
"auth_user_button_add": "Ajouter un utilisateur",
"auth_user_panel_active": "Utilisateurs actifs",
"auth_user_panel_inactive": "Utilisateurs inactifs",
"auth_user_panel_empty": "Actuellement, il n'y a pas d'utilisateurs. %link% maintenant.",
"auth_user_panel_th_username": "Nom d'utilisateur",
"auth_user_panel_th_enabled": "Activé",
"auth_user_panel_th_activity": "Options",
"auth_user_form_add_title": "Ajout d'un utilisateur",
"auth_user_form_add_submit": "Ajouter",
"auth_user_form_edit_title": "Modification d'un utilisateur",
"auth_user_form_edit_submit": "Enregistrer",
"auth_user_form_label_username": "Nom d'utilisateur",
"auth_user_form_label_password": "Mot de passe",
"auth_user_form_button_cancel": "Annuler",
"js_auth_user_delete_confirmation": "Êtes-vous sûr ?",
"settings_page_title": "Paramètres",
"settings_variable_panel_system_variables": "Paramètres généraux",
"settings_variable_panel_plugin_variables": "Paramètres des plugins",
@ -68,6 +86,7 @@
"settings_variable_form_button_cancel": "Annuler",
"settings_variable_desc_lang": "Langage de l'application",
"settings_variable_desc_fleet_enabled": "Activer la gestion de flotte des écrans",
"settings_variable_desc_auth_enabled": "Activer la gestion de l'authentification",
"settings_variable_desc_external_url": "URL externe (i.e: https://screen-01.company.com or http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Limite d'upload du fichier d'une slide (en octets, 32*1024*1024 pour 32Mo)",
"settings_variable_desc_default_slide_duration": "Durée de la slide d'introduction (en secondes)",
@ -115,15 +134,16 @@
"common_unknown_ipaddr": "Adresse IP inconnue",
"common_empty": "[Vide]",
"logout": "Déconnexion",
"enum_animation_speed_slower": "Très lent",
"enum_animation_speed_slow": "Lent",
"enum_animation_speed_normal": "Normal",
"enum_animation_speed_fast": "Rapide",
"enum_animation_speed_faster": "Très rapide",
"enum_variable_section_general": "Général",
"enum_variable_section_player_animation": "Animation du lecteur",
"enum_variable_section_player_options": "Options du lecteur",
"enum_variable_section_general": "1. Général",
"enum_variable_section_player_options": "2. Options du lecteur",
"enum_variable_section_player_animation": "3. Animation du lecteur",
"enum_application_language_english": "Anglais",
"enum_application_language_french": "Français",
"enum_slide_type_url": "URL",

View File

@ -5,7 +5,7 @@ from src.interface.ObController import ObController
# class FooController(ObController):
# def register(self):
# self._app.add_url_rule('/foo', 'foo', self.foo, methods=['GET'])
# self._app.add_url_rule('/foo', 'foo', self._auth(self.foo), methods=['GET'])
# self._app.add_url_rule('/foo_html', 'foo_html', self.foo_html, methods=['GET'])
#
# def foo(self):

View File

@ -3,3 +3,4 @@ pysondb-v2==2.1.0
python-dotenv
cron-descriptor
waitress
flask-login

View File

@ -0,0 +1,76 @@
import json
from flask import Flask, render_template, redirect, request, url_for, jsonify
from flask_login import login_user, logout_user
from src.service.ModelStore import ModelStore
from src.model.entity.User import User
from src.interface.ObController import ObController
class AuthController(ObController):
def register(self):
self._app.add_url_rule('/login', 'login', self.login, methods=['GET', 'POST'])
self._app.add_url_rule('/logout', 'logout', self.logout, methods=['GET'])
self._app.add_url_rule('/auth/user/list', 'auth_user_list', self._auth(self.auth_user_list), methods=['GET'])
self._app.add_url_rule('/auth/user/add', 'auth_user_add', self._auth(self.auth_user_add), methods=['POST'])
self._app.add_url_rule('/auth/user/edit', 'auth_user_edit', self._auth(self.auth_user_edit), methods=['POST'])
self._app.add_url_rule('/auth/user/toggle', 'auth_user_toggle', self._auth(self.auth_user_toggle), methods=['POST'])
self._app.add_url_rule('/auth/user/delete', 'auth_user_delete', self._auth(self.auth_user_delete), methods=['DELETE'])
def login(self):
login_error = None
if len(request.form):
user = self._model_store.user().get_one_by_username(request.form['username'], enabled=True)
if user:
if user.password == self._model_store.user().encode_password(request.form['password']):
login_user(user)
return redirect(url_for('slideshow_slide_list'))
else:
login_error = 'bad_credentials'
else:
login_error = 'not_found'
return render_template(
'auth/login.jinja.html',
login_error=login_error
)
def logout(self):
logout_user()
return redirect(url_for('login'))
def auth_user_list(self):
return render_template(
'auth/list.jinja.html',
enabled_users=self._model_store.user().get_enabled_users(),
disabled_users=self._model_store.user().get_disabled_users(),
)
def auth_user_add(self):
self._model_store.user().add_form(User(
username=request.form['username'],
password=request.form['password'],
enabled=True,
))
return redirect(url_for('auth_user_list'))
def auth_user_edit(self):
self._model_store.user().update_form(
id=request.form['id'],
username=request.form['username'],
password=request.form['password'] if 'password' in request.form else None
)
return redirect(url_for('auth_user_list'))
def auth_user_toggle(self):
data = request.get_json()
self._model_store.user().update_enabled(data.get('id'), data.get('enabled'))
return jsonify({'status': 'ok'})
def auth_user_delete(self):
data = request.get_json()
self._model_store.user().delete(data.get('id'))
return jsonify({'status': 'ok'})

View File

@ -9,13 +9,13 @@ from src.interface.ObController import ObController
class FleetController(ObController):
def register(self):
self._app.add_url_rule('/fleet', 'fleet', self.fleet, methods=['GET'])
self._app.add_url_rule('/fleet/screen/list', 'fleet_screen_list', self.fleet_screen_list, methods=['GET'])
self._app.add_url_rule('/fleet/screen/add', 'fleet_screen_add', self.fleet_screen_add, methods=['POST'])
self._app.add_url_rule('/fleet/screen/edit', 'fleet_screen_edit', self.fleet_screen_edit, methods=['POST'])
self._app.add_url_rule('/fleet/screen/toggle', 'fleet_screen_toggle', self.fleet_screen_toggle, methods=['POST'])
self._app.add_url_rule('/fleet/screen/delete', 'fleet_screen_delete', self.fleet_screen_delete, methods=['DELETE'])
self._app.add_url_rule('/fleet/screen/position', 'fleet_screen_position', self.fleet_screen_position, methods=['POST'])
self._app.add_url_rule('/fleet', 'fleet', self._auth(self.fleet), methods=['GET'])
self._app.add_url_rule('/fleet/screen/list', 'fleet_screen_list', self._auth(self.fleet_screen_list), methods=['GET'])
self._app.add_url_rule('/fleet/screen/add', 'fleet_screen_add', self._auth(self.fleet_screen_add), methods=['POST'])
self._app.add_url_rule('/fleet/screen/edit', 'fleet_screen_edit', self._auth(self.fleet_screen_edit), methods=['POST'])
self._app.add_url_rule('/fleet/screen/toggle', 'fleet_screen_toggle', self._auth(self.fleet_screen_toggle), methods=['POST'])
self._app.add_url_rule('/fleet/screen/delete', 'fleet_screen_delete', self._auth(self.fleet_screen_delete), methods=['DELETE'])
self._app.add_url_rule('/fleet/screen/position', 'fleet_screen_position', self._auth(self.fleet_screen_position), methods=['POST'])
def fleet(self):
return render_template(

View File

@ -9,14 +9,14 @@ from src.interface.ObController import ObController
class SettingsController(ObController):
def register(self):
self._app.add_url_rule('/settings/variable/list', 'settings_variable_list', self.settings_variable_list, methods=['GET'])
self._app.add_url_rule('/settings/variable/edit', 'settings_variable_edit', self.settings_variable_edit, methods=['POST'])
self._app.add_url_rule('/settings/variable/list', 'settings_variable_list', self._auth(self.settings_variable_list), methods=['GET'])
self._app.add_url_rule('/settings/variable/edit', 'settings_variable_edit', self._auth(self.settings_variable_edit), methods=['POST'])
def settings_variable_list(self):
return render_template(
'settings/list.jinja.html',
system_variables=self._model_store.variable().get_editable_variables(plugin=False),
plugin_variables=self._model_store.variable().get_editable_variables(plugin=True),
system_variables=self._model_store.variable().get_editable_variables(plugin=False, sort='section'),
plugin_variables=self._model_store.variable().get_editable_variables(plugin=True, sort='plugin'),
)
def settings_variable_edit(self):

View File

@ -15,13 +15,13 @@ class SlideshowController(ObController):
def register(self):
self._app.add_url_rule('/manage', 'manage', self.manage, methods=['GET'])
self._app.add_url_rule('/slideshow', 'slideshow_slide_list', self.slideshow, methods=['GET'])
self._app.add_url_rule('/slideshow/slide/add', 'slideshow_slide_add', self.slideshow_slide_add, methods=['POST'])
self._app.add_url_rule('/slideshow/slide/edit', 'slideshow_slide_edit', self.slideshow_slide_edit, methods=['POST'])
self._app.add_url_rule('/slideshow/slide/toggle', 'slideshow_slide_toggle', self.slideshow_slide_toggle, methods=['POST'])
self._app.add_url_rule('/slideshow/slide/delete', 'slideshow_slide_delete', self.slideshow_slide_delete, methods=['DELETE'])
self._app.add_url_rule('/slideshow/slide/position', 'slideshow_slide_position', self.slideshow_slide_position, methods=['POST'])
self._app.add_url_rule('/slideshow/player-refresh', 'slideshow_player_refresh', self.slideshow_player_refresh, methods=['GET'])
self._app.add_url_rule('/slideshow', 'slideshow_slide_list', self._auth(self.slideshow), methods=['GET'])
self._app.add_url_rule('/slideshow/slide/add', 'slideshow_slide_add', self._auth(self.slideshow_slide_add), methods=['POST'])
self._app.add_url_rule('/slideshow/slide/edit', 'slideshow_slide_edit', self._auth(self.slideshow_slide_edit), methods=['POST'])
self._app.add_url_rule('/slideshow/slide/toggle', 'slideshow_slide_toggle', self._auth(self.slideshow_slide_toggle), methods=['POST'])
self._app.add_url_rule('/slideshow/slide/delete', 'slideshow_slide_delete', self._auth(self.slideshow_slide_delete), methods=['DELETE'])
self._app.add_url_rule('/slideshow/slide/position', 'slideshow_slide_position', self._auth(self.slideshow_slide_position), methods=['POST'])
self._app.add_url_rule('/slideshow/player-refresh', 'slideshow_player_refresh', self._auth(self.slideshow_player_refresh), methods=['GET'])
def manage(self):
return redirect(url_for('slideshow_slide_list'))

View File

@ -15,9 +15,9 @@ from src.utils import get_ip_address
class SysinfoController(ObController):
def register(self):
self._app.add_url_rule('/sysinfo', 'sysinfo_attribute_list', self.sysinfo, methods=['GET'])
self._app.add_url_rule('/sysinfo/restart', 'sysinfo_restart', self.sysinfo_restart, methods=['POST'])
self._app.add_url_rule('/sysinfo/restart/needed', 'sysinfo_restart_needed', self.sysinfo_restart_needed, methods=['GET'])
self._app.add_url_rule('/sysinfo', 'sysinfo_attribute_list', self._auth(self.sysinfo), methods=['GET'])
self._app.add_url_rule('/sysinfo/restart', 'sysinfo_restart', self._auth(self.sysinfo_restart), methods=['POST'])
self._app.add_url_rule('/sysinfo/restart/needed', 'sysinfo_restart_needed', self._auth(self.sysinfo_restart_needed), methods=['GET'])
def sysinfo(self):
ipaddr = get_ip_address()

View File

@ -8,8 +8,9 @@ from src.interface.ObPlugin import ObPlugin
class ObController(abc.ABC):
def __init__(self, app, model_store: ModelStore, template_renderer: TemplateRenderer, plugin: Optional[ObPlugin] = None):
def __init__(self, app, auth_required, model_store: ModelStore, template_renderer: TemplateRenderer, plugin: Optional[ObPlugin] = None):
self._app = app
self._auth = auth_required
self._model_store = model_store
self._template_renderer = template_renderer
self._plugin = plugin

View File

@ -24,6 +24,7 @@ class ConfigManager:
'log_file': None,
'log_level': 'INFO',
'log_stdout': True,
'secret_key': 'ANY_SECRET_KEY_HERE',
'player_url': 'http://localhost:{}'.format(self.DEFAULT_PORT)
}
@ -46,6 +47,7 @@ class ConfigManager:
parser.add_argument('--debug', '-d', default=self._CONFIG['debug'], help='Debug mode')
parser.add_argument('--port', '-p', default=self._CONFIG['port'], help='Application port')
parser.add_argument('--bind', '-b', default=self._CONFIG['bind'], help='Application bind address')
parser.add_argument('--secret-key', '-s', default=self._CONFIG['secret_key'], help='Application secret key (any random string)')
parser.add_argument('--autoconfigure-lx-file', '-x', default=self._CONFIG['autoconfigure_lx_file'], help='Path to lx autostart file')
parser.add_argument('--log-file', '-lf', default=self._CONFIG['log_file'], help='Log File path')
parser.add_argument('--log-level', '-ll', default=self._CONFIG['log_level'], help='Log Level')
@ -67,6 +69,8 @@ class ConfigManager:
self._CONFIG['autoconfigure_lx_file'] = args.autoconfigure_lx_file
if args.log_file:
self._CONFIG['log_file'] = args.log_file
if args.secret_key:
self._CONFIG['secret_key'] = args.secret_key
if args.log_level:
self._CONFIG['log_level'] = args.log_level
if args.log_stdout:

104
src/manager/UserManager.py Normal file
View File

@ -0,0 +1,104 @@
import hashlib
from pysondb.errors import IdDoesNotExistError
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.User import User
from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager
from src.service.ModelManager import ModelManager
class UserManager(ModelManager):
TABLE_NAME = "user"
TABLE_MODEL = [
"username",
"password",
"enabled"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager):
super().__init__(lang_manager, database_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@staticmethod
def hydrate_object(raw_user: dict, id: Optional[str] = None) -> User:
if id:
raw_user['id'] = id
return User(**raw_user)
@staticmethod
def hydrate_dict(raw_users: dict) -> List[User]:
return [UserManager.hydrate_object(raw_user, raw_id) for raw_id, raw_user in raw_users.items()]
@staticmethod
def hydrate_list(raw_users: list) -> List[User]:
return [UserManager.hydrate_object(raw_user) for raw_user in raw_users]
def get(self, id: str) -> Optional[User]:
try:
return self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get_by(self, query) -> List[User]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_one_by(self, query) -> Optional[User]:
users = self.hydrate_dict(self._db.get_by_query(query=query))
if len(users) == 1:
return users[0]
elif len(users) > 1:
raise Error("More than one result for query")
return None
def get_one_by_username(self, username: str, enabled: bool = None) -> Optional[User]:
return self.get_one_by(query=lambda v: v['username'] == username and (enabled is None or v['enabled'] == enabled))
def get_all(self, sort: bool = False) -> List[User]:
raw_users = self._db.get_all()
if isinstance(raw_users, dict):
if sort:
return sorted(UserManager.hydrate_dict(raw_users), key=lambda x: x.username)
return UserManager.hydrate_dict(raw_users)
return UserManager.hydrate_list(sorted(raw_users, key=lambda x: x['username']) if sort else raw_users)
def get_enabled_users(self) -> List[User]:
return [user for user in self.get_all(sort=True) if user.enabled]
def get_disabled_users(self) -> List[User]:
return [user for user in self.get_all(sort=True) if not user.enabled]
def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, {"enabled": enabled})
def update_form(self, id: str, username: str, password: Optional[str]) -> None:
form = {"username": username}
if password is not None and password:
form['password'] = self.encode_password(password)
self._db.update_by_id(id, form)
def add_form(self, user: Union[User, Dict]) -> None:
form = user
if not isinstance(user, dict):
form = user.to_dict()
del form['id']
form['password'] = self.encode_password(form['password'])
self._db.add(form)
def delete(self, id: str) -> None:
self._db.delete_by_id(id)
def to_dict(self, users: List[User]) -> List[Dict]:
return [user.to_dict() for user in users]
def encode_password(self, password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()

View File

@ -97,6 +97,7 @@ class VariableManager(ModelManager):
### General
{"name": "lang", "section": self.t(VariableSection.GENERAL), "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_lang'), "selectables": self.t(ApplicationLanguage), "refresh_player": False},
{"name": "auth_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_auth_enabled'), "refresh_player": False},
{"name": "fleet_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_fleet_enabled'), "refresh_player": False},
{"name": "external_url", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": True, "description": self.t('settings_variable_desc_external_url'), "refresh_player": False},
{"name": "slide_upload_limit", "section": self.t(VariableSection.GENERAL), "value": 32 * 1024 * 1024, "unit": VariableUnit.BYTE, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_slide_upload_limit'), "refresh_player": False},
@ -190,9 +191,12 @@ class VariableManager(ModelManager):
return VariableManager.hydrate_list(raw_variables)
def get_editable_variables(self, plugin: bool = True) -> List[Variable]:
def get_editable_variables(self, plugin: bool = True, sort: Optional[str] = None) -> List[Variable]:
query = lambda v: (not plugin and not isinstance(v['plugin'], str)) or (plugin and isinstance(v['plugin'], str))
return [variable for variable in self.get_by(query=query) if variable.editable]
variables = [variable for variable in self.get_by(query=query) if variable.editable]
if sort is not None and sort:
return sorted(variables, key=lambda x: getattr(x, sort))
return variables
def get_readonly_variables(self) -> List[Variable]:
return [variable for variable in self.get_all() if not variable.editable]

70
src/model/entity/User.py Normal file
View File

@ -0,0 +1,70 @@
import json
from typing import Optional, Union
class User:
def __init__(self, username: str = '', password: str = '', enabled: bool = True, id: Optional[str] = None):
self._id = id if id else None
self._username = username
self._password = password
self._enabled = enabled
@property
def id(self) -> Union[int, str]:
return self._id
@property
def username(self) -> str:
return self._username
@username.setter
def username(self, value: str):
self._username = value
@property
def password(self) -> str:
return self._password
@password.setter
def password(self, value: str):
self._password = value
@property
def enabled(self) -> bool:
return self._enabled
@enabled.setter
def enabled(self, value: bool):
self._enabled = value
def __str__(self) -> str:
return f"User(" \
f"id='{self.id}',\n" \
f"username='{self.username}',\n" \
f"enabled='{self.enabled}',\n" \
f")"
def to_json(self) -> str:
return json.dumps(self.to_dict())
def to_dict(self) -> dict:
return {
"id": self.id,
"username": self.username,
"password": self.password,
"enabled": self.enabled,
}
def is_authenticated(self):
return True
def is_active(self):
return self.enabled
def is_anonymous(self):
return False
def get_id(self):
return self.id

View File

@ -15,6 +15,14 @@ class HookType(Enum):
H_FLEET_CSS = 'h_fleet_css'
H_FLEET_JAVASCRIPT = 'h_fleet_javascript'
H_AUTH_TOOLBAR_ACTIONS_START = 'h_auth_toolbar_actions_start'
H_AUTH_TOOLBAR_ACTIONS_END = 'h_auth_toolbar_actions_end'
H_AUTH_CSS = 'h_auth_css'
H_AUTH_JAVASCRIPT = 'h_auth_javascript'
H_LOGIN_CSS = 'h_login_css'
H_LOGIN_JAVASCRIPT = 'h_login_javascript'
H_ROOT_CSS = 'h_root_css'
H_ROOT_JAVASCRIPT = 'h_root_javascript'
H_ROOT_NAV_ELEMENT_START = 'h_root_nav_element_start'

View File

@ -1,5 +1,6 @@
from src.manager.SlideManager import SlideManager
from src.manager.ScreenManager import ScreenManager
from src.manager.UserManager import UserManager
from src.manager.VariableManager import VariableManager
from src.manager.LangManager import LangManager
from src.manager.DatabaseManager import DatabaseManager
@ -23,6 +24,7 @@ class ModelStore:
self._logging_manager = LoggingManager(config_manager=self._config_manager)
# Model
self._user_manager = UserManager(lang_manager=self._lang_manager, database_manager=self._database_manager)
self._screen_manager = ScreenManager(lang_manager=self._lang_manager, database_manager=self._database_manager)
self._slide_manager = SlideManager(lang_manager=self._lang_manager, database_manager=self._database_manager)
self._variable_manager.reload()
@ -48,3 +50,6 @@ class ModelStore:
def lang(self) -> LangManager:
return self._lang_manager
def user(self) -> UserManager:
return self._user_manager

View File

@ -26,6 +26,7 @@ class TemplateRenderer:
globals = dict(
STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS),
FLEET_ENABLED=self._model_store.variable().map().get('fleet_enabled').as_bool(),
AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(),
VERSION=self._model_store.config().map().get('version'),
LANG=self._model_store.variable().map().get('lang').as_string(),
HOOK=self._render_hook,

View File

@ -2,12 +2,17 @@ import os
import time
from waitress import serve
from flask import Flask, send_from_directory
from flask import Flask, send_from_directory, redirect, url_for
from flask_login import LoginManager, current_user
from src.model.entity.User import User
from src.manager.UserManager import UserManager
from src.service.ModelStore import ModelStore
from src.service.TemplateRenderer import TemplateRenderer
from src.controller.PlayerController import PlayerController
from src.controller.SlideshowController import SlideshowController
from src.controller.FleetController import FleetController
from src.controller.AuthController import AuthController
from src.controller.SysinfoController import SysinfoController
from src.controller.SettingsController import SettingsController
from src.constant.WebDirConstant import WebDirConstant
@ -16,6 +21,8 @@ from src.constant.WebDirConstant import WebDirConstant
class WebServer:
def __init__(self, project_dir: str, model_store: ModelStore, template_renderer: TemplateRenderer):
self._app = None
self._login_manager = None
self._project_dir = project_dir
self._model_store = model_store
self._template_renderer = template_renderer
@ -54,17 +61,50 @@ class WebServer:
self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS)
self._app.config['MAX_CONTENT_LENGTH'] = self._model_store.variable().map().get('slide_upload_limit').as_int()
self._setup_flask_login()
if self._debug:
self._app.config['TEMPLATES_AUTO_RELOAD'] = True
def _setup_flask_login(self) -> bool:
auth_module = self._model_store.variable().map().get('auth_enabled').as_bool()
if not auth_module:
return auth_module
self._app.config['SECRET_KEY'] = self._model_store.config().map().get('secret_key')
self._login_manager = LoginManager()
self._login_manager.init_app(self._app)
self._login_manager.login_view = 'login'
@self._login_manager.user_loader
def load_user(user_id):
return self._model_store.user().get(user_id)
return auth_module
def _setup_web_controllers(self) -> None:
PlayerController(self._app, self._model_store, self._template_renderer)
SlideshowController(self._app, self._model_store, self._template_renderer)
SettingsController(self._app, self._model_store, self._template_renderer)
SysinfoController(self._app, self._model_store, self._template_renderer)
def auth_required(f):
if not self._login_manager:
return f
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
PlayerController(self._app, auth_required, self._model_store, self._template_renderer)
SlideshowController(self._app, auth_required, self._model_store, self._template_renderer)
SettingsController(self._app, auth_required, self._model_store, self._template_renderer)
SysinfoController(self._app, auth_required, self._model_store, self._template_renderer)
if self._model_store.variable().map().get('fleet_enabled').as_bool():
FleetController(self._app, self._model_store, self._template_renderer)
FleetController(self._app, auth_required, self._model_store, self._template_renderer)
if self._login_manager:
AuthController(self._app, auth_required, self._model_store, self._template_renderer)
def _setup_web_globals(self) -> None:
@self._app.context_processor

View File

@ -9,5 +9,5 @@
@unclutter -display :0 -noevents -grab
@sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' ~/.config/chromium/Default/Preferences
#@sleep 10
@chromium-browser --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --disable-restore-session-state --noerrdialogs --kiosk --incognito --window-position=0,0 --display=:0 http://localhost:5001
@chromium-browser --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --disable-restore-session-state --noerrdialogs --kiosk --incognito --window-position=0,0 --display=:0 http://localhost:5000

View File

@ -0,0 +1,42 @@
<table class="{{ tclass }}-users">
<thead>
<tr>
<th>{{ l.auth_user_panel_th_username }}</th>
<th class="tac">{{ l.auth_user_panel_th_enabled }}</th>
<th class="tac">{{ l.auth_user_panel_th_activity }}</th>
</tr>
</thead>
<tbody>
<tr class="empty-tr {% if users|length != 0 %}hidden{% endif %}">
<td colspan="4">
{{ l.auth_user_panel_empty|replace(
'%link%',
('<a href="javascript:void(0);" class="item-add user-add">'~l.auth_user_button_add~'</a>')|safe
) }}
</td>
</tr>
{% for user in users %}
<tr class="user-item" data-level="{{ user.id }}" data-entity="{{ user.to_json() }}">
<td class="infos">
<div class="inner">
<i class="fa fa-user icon-left"></i>
{{ user.username }}
</div>
</td>
<td class="tac">
<label class="pure-material-switch">
<input type="checkbox" {% if user.enabled %}checked="checked"{% endif %}><span></span>
</label>
</td>
<td class="actions tac">
<a href="javascript:void(0);" class="item-edit user-edit">
<i class="fa fa-pencil"></i>
</a>
<a href="javascript:void(0);" class="item-delete user-delete">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,57 @@
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.auth_page_title }}
{% endblock %}
{% block add_css %}
{{ HOOK(H_AUTH_CSS) }}
{% endblock %}
{% block add_js %}
<script src="{{ STATIC_PREFIX }}js/auth.js"></script>
{{ HOOK(H_AUTH_JAVASCRIPT) }}
{% endblock %}
{% block page %}
<div class="toolbar">
<h2>{{ l.auth_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_START) }}
<button class="purple user-add item-add"><i class="fa fa-plus icon-left"></i>{{ l.auth_user_button_add }}</button>
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_END) }}
</div>
</div>
<div class="panel">
<div class="panel-body">
<h3>{{ l.auth_user_panel_active }}</h3>
{% with tclass='active', users=enabled_users %}
{% include 'auth/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
<div class="panel panel-inactive">
<div class="panel-body">
<h3>{{ l.auth_user_panel_inactive }}</h3>
{% with tclass='inactive', users=disabled_users %}
{% include 'auth/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
<div class="modals hidden">
<div class="modals-outer">
<a href="javascript:void(0);" class="modal-close">
<i class="fa fa-close"></i>
</a>
<div class="modals-inner">
{% include 'auth/modal/add.jinja.html' %}
{% include 'auth/modal/edit.jinja.html' %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.login_page_title }}
{% endblock %}
{% block add_css %}
{{ HOOK(H_LOGIN_CSS) }}
{% endblock %}
{% block add_js %}
{{ HOOK(H_LOGIN_JAVASCRIPT) }}
{% endblock %}
{% block page %}
Hello
<form action="{{ url_for('login') }}" method="post">
error: {{ login_error }}
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
{% endblock %}

View File

@ -0,0 +1,30 @@
<div class="modal modal-user-add">
<h2>
{{ l.auth_user_form_add_title }}
</h2>
<form action="/auth/user/add" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="user-add-name">{{ l.auth_user_form_label_username }}</label>
<div class="widget">
<input name="username" type="text" id="user-add-username" required="required" />
</div>
</div>
<div class="form-group">
<label for="user-add-password">{{ l.auth_user_form_label_password }}</label>
<div class="widget">
<input type="password" name="password" id="user-add-password" required="required" />
</div>
</div>
<div class="actions">
<button type="button" class="modal-close">
{{ l.auth_user_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-plus icon-left"></i> {{ l.auth_user_form_add_submit }}
</button>
</div>
</form>
</div>

View File

@ -0,0 +1,32 @@
<div class="modal modal-user-edit hidden">
<h2>
{{ l.auth_user_form_edit_submit }}
</h2>
<form action="/auth/user/edit" method="POST">
<input type="hidden" name="id" id="user-edit-id" />
<div class="form-group">
<label for="user-edit-username">{{ l.auth_user_form_label_username }}</label>
<div class="widget">
<input type="text" name="username" id="user-edit-username" required="required" />
</div>
</div>
<div class="form-group">
<label for="user-edit-password">{{ l.auth_user_form_label_password }}</label>
<div class="widget">
<input type="password" name="password" id="user-edit-password" />
</div>
</div>
<div class="actions">
<button type="button" class="modal-close">
{{ l.auth_user_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-save icon-left"></i>{{ l.auth_user_form_edit_submit }}
</button>
</div>
</form>
</div>

View File

@ -46,6 +46,7 @@
Obscreen
</a>
</h1>
{% if (current_user and current_user.is_authenticated) or not current_user %}
<nav>
<ul>
{{ HOOK(H_ROOT_NAV_ELEMENT_START) }}
@ -61,6 +62,13 @@
</a>
</li>
{% endif %}
{% if AUTH_ENABLED %}
<li class="{{ 'active' if request.url_rule.endpoint == 'auth_user_list' }}">
<a href="{{ url_for('auth_user_list') }}">
<i class="fa fa-user"></i> {{ l.auth_page_title }}
</a>
</li>
{% endif %}
<li class="{{ 'active' if request.url_rule.endpoint == 'settings_variable_list' }}">
<a href="{{ url_for('settings_variable_list') }}">
<i class="fa-solid fa-cogs"></i> {{ l.settings_page_title }}
@ -71,9 +79,17 @@
<i class="fa-solid fa-list-check"></i> {{ l.sysinfo_page_title }}
</a>
</li>
{% if AUTH_ENABLED %}
<li>
<a href="{{ url_for('logout') }}" title="{{ l.logout }}">
<i class="fa fa-right-from-bracket"></i>
</a>
</li>
{% endif %}
{{ HOOK(H_ROOT_NAV_ELEMENT_END) }}
</ul>
</nav>
{% endif %}
</header>
{% endif %}
{% endblock %}