auth 99% ok missing frontend style
This commit is contained in:
parent
885c30ded3
commit
a9c899f410
@ -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
106
data/www/js/auth.js
Normal 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();
|
||||
});
|
||||
26
lang/en.json
26
lang/en.json
@ -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",
|
||||
|
||||
26
lang/fr.json
26
lang/fr.json
@ -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",
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -3,3 +3,4 @@ pysondb-v2==2.1.0
|
||||
python-dotenv
|
||||
cron-descriptor
|
||||
waitress
|
||||
flask-login
|
||||
|
||||
76
src/controller/AuthController.py
Normal file
76
src/controller/AuthController.py
Normal 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'})
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
104
src/manager/UserManager.py
Normal 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()
|
||||
@ -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
70
src/model/entity/User.py
Normal 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
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
42
views/auth/component/table.jinja.html
Normal file
42
views/auth/component/table.jinja.html
Normal 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>
|
||||
57
views/auth/list.jinja.html
Normal file
57
views/auth/list.jinja.html
Normal 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 %}
|
||||
23
views/auth/login.jinja.html
Normal file
23
views/auth/login.jinja.html
Normal 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 %}
|
||||
30
views/auth/modal/add.jinja.html
Normal file
30
views/auth/modal/add.jinja.html
Normal 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>
|
||||
32
views/auth/modal/edit.jinja.html
Normal file
32
views/auth/modal/edit.jinja.html
Normal 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>
|
||||
@ -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 %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user