diff --git a/app/__init__.py b/app/__init__.py index 8bafbd1..458e508 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -121,13 +121,22 @@ def create_app(config_name=None): @app.context_processor def inject_globals(): from flask_login import current_user + from app.models import SystemSetting unread = 0 if current_user.is_authenticated: from app.models import Notification unread = Notification.query.filter_by( user_id=current_user.id, is_read=False ).count() - return dict(unread_notifications=unread) + branding = { + 'app_name' : SystemSetting.get('app_name', 'TechDesk'), + 'app_subtitle' : SystemSetting.get('app_subtitle', 'IT Helpdesk System'), + 'company_name' : SystemSetting.get('company_name', ''), + 'logo_stored_name' : SystemSetting.get('logo_stored_name', ''), + 'logo_initials' : SystemSetting.get('logo_initials', 'TD'), + 'primary_color' : SystemSetting.get('primary_color', '#2563eb'), + } + return dict(unread_notifications=unread, branding=branding) # ── DB initialisation (first run) ───────────────────────────────────────── with app.app_context(): @@ -142,7 +151,13 @@ def _seed_settings(): """Ensure all required system settings exist with safe defaults.""" from app.models import SystemSetting defaults = [ - ('registration_enabled', 'true', 'Allow new users to self-register via /auth/register'), + ('registration_enabled', 'true', 'Allow new users to self-register via /auth/register'), + ('app_name', 'TechDesk', 'Application name shown in the sidebar and page titles'), + ('app_subtitle', 'IT Helpdesk System', 'Subtitle shown below the app name in the sidebar'), + ('company_name', '', 'Company name shown on the login and register pages'), + ('logo_stored_name', '', 'Stored filename of the uploaded company logo image'), + ('logo_initials', 'TD', 'Two-letter initials shown when no logo is uploaded'), + ('primary_color', '#2563eb', 'Primary accent colour (hex) used across the interface'), ] for key, value, description in defaults: if SystemSetting.get(key) is None: diff --git a/app/routes/admin.py b/app/routes/admin.py index d1b18f1..7a009d1 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -851,29 +851,95 @@ def settings(): from app.models import SystemSetting if request.method == 'POST': - new_value = '1' if request.form.get('registration_enabled') == '1' else '0' - old_value = SystemSetting.get('registration_enabled', 'true') - SystemSetting.set( - 'registration_enabled', - new_value, - 'Allow new users to self-register via /auth/register' - ) - db.session.commit() + form_type = request.form.get('form_type') - state_label = 'enabled' if new_value == '1' else 'disabled' - log_action( - current_user.id, - 'setting_update', - 'system_setting', - None, - f'registration_enabled changed from {old_value} to {new_value} by admin_id={current_user.id}' - ) - logger.info( - f'[ADMIN SETTINGS] registration_enabled={new_value} ' - f'by admin_id={current_user.id} email={current_user.email}' - ) - flash(f'User registration has been {state_label}.', 'success') - return redirect(url_for('admin.settings')) + # ── Registration toggle ─────────────────────────────────────────────── + if form_type == 'registration': + new_value = '1' if request.form.get('registration_enabled') == '1' else '0' + old_value = SystemSetting.get('registration_enabled', 'true') + SystemSetting.set('registration_enabled', new_value, + 'Allow new users to self-register via /auth/register') + db.session.commit() + state_label = 'enabled' if new_value == '1' else 'disabled' + log_action(current_user.id, 'setting_update', 'system_setting', None, + f'registration_enabled changed from {old_value} to {new_value} by admin_id={current_user.id}') + logger.info(f'[ADMIN SETTINGS] registration_enabled={new_value} by admin_id={current_user.id}') + flash(f'User registration has been {state_label}.', 'success') + return redirect(url_for('admin.settings')) + + # ── Branding update ─────────────────────────────────────────────────── + if form_type == 'branding': + fields = { + 'app_name' : ('Application name', request.form.get('app_name', '').strip()), + 'app_subtitle' : ('App subtitle', request.form.get('app_subtitle', '').strip()), + 'company_name' : ('Company name', request.form.get('company_name', '').strip()), + 'logo_initials': ('Logo initials', request.form.get('logo_initials', '').strip()[:3]), + 'primary_color': ('Primary colour', request.form.get('primary_color', '#2563eb').strip()), + } + changes = [] + for key, (label, new_val) in fields.items(): + old_val = SystemSetting.get(key, '') + if new_val != old_val: + SystemSetting.set(key, new_val) + changes.append(f'{key}={new_val!r}') + + # ── Logo file upload ────────────────────────────────────────────── + logo_file = request.files.get('logo_file') + if logo_file and logo_file.filename: + from app.services.validation_service import validate_file + from werkzeug.utils import secure_filename + allowed = {'png', 'jpg', 'jpeg', 'gif', 'webp'} + file_error = validate_file(logo_file, allowed) + if file_error: + flash(f'Logo not saved: {file_error}', 'danger') + else: + logo_file.stream.seek(0, 2) + logo_size = logo_file.stream.tell() + logo_file.stream.seek(0) + if logo_size > 2 * 1024 * 1024: + flash('Logo image must be under 2 MB.', 'danger') + else: + ext = secure_filename(logo_file.filename).rsplit('.', 1)[-1].lower() + stored_name = f"logo_{uuid.uuid4().hex}.{ext}" + upload_dir = current_app.config['UPLOAD_FOLDER'] + # Delete old logo from disk + old_logo = SystemSetting.get('logo_stored_name', '') + if old_logo: + old_path = os.path.join(upload_dir, old_logo) + if os.path.exists(old_path): + os.remove(old_path) + logo_file.save(os.path.join(upload_dir, stored_name)) + SystemSetting.set('logo_stored_name', stored_name, 'Stored filename of the uploaded company logo image') + changes.append(f'logo_stored_name={stored_name!r}') + logger.info(f'[ADMIN BRANDING] logo uploaded file={stored_name} by admin_id={current_user.id}') + + # ── Logo removal ────────────────────────────────────────────────── + if request.form.get('remove_logo') == '1': + old_logo = SystemSetting.get('logo_stored_name', '') + if old_logo: + old_path = os.path.join(current_app.config['UPLOAD_FOLDER'], old_logo) + if os.path.exists(old_path): + os.remove(old_path) + SystemSetting.set('logo_stored_name', '') + changes.append('logo_stored_name removed') + + db.session.commit() + if changes: + log_action(current_user.id, 'branding_update', 'system_setting', None, + f'changes: {", ".join(changes)} by admin_id={current_user.id}') + logger.info(f'[ADMIN BRANDING] {", ".join(changes)} by admin_id={current_user.id}') + flash('Branding settings saved.', 'success') + return redirect(url_for('admin.settings')) registration_enabled = SystemSetting.get_bool('registration_enabled', default=True) - return render_template('admin/settings.html', registration_enabled=registration_enabled) \ No newline at end of file + branding_settings = { + 'app_name' : SystemSetting.get('app_name', 'TechDesk'), + 'app_subtitle' : SystemSetting.get('app_subtitle', 'IT Helpdesk System'), + 'company_name' : SystemSetting.get('company_name', ''), + 'logo_stored_name': SystemSetting.get('logo_stored_name', ''), + 'logo_initials' : SystemSetting.get('logo_initials', 'TD'), + 'primary_color' : SystemSetting.get('primary_color', '#2563eb'), + } + return render_template('admin/settings.html', + registration_enabled=registration_enabled, + branding_settings=branding_settings) \ No newline at end of file diff --git a/app/routes/auth.py b/app/routes/auth.py index dcb5da2..a1534c7 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -17,6 +17,9 @@ logger = logging.getLogger(__name__) AVATAR_ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'gif', 'webp'} AVATAR_MAX_BYTES = 5 * 1024 * 1024 # 5 MB +LOGO_ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'} +LOGO_MAX_BYTES = 2 * 1024 * 1024 # 2 MB + def _is_safe_url(target): """Return True only when *target* points back to this same host. @@ -195,3 +198,12 @@ def serve_avatar(filename): abort(400) upload_dir = current_app.config['UPLOAD_FOLDER'] return send_from_directory(upload_dir, filename, as_attachment=False) + + +@auth_bp.route('/logo/') +def serve_logo(filename): + """Serve the company logo — publicly accessible (shown on login page).""" + if '/' in filename or '\\' in filename or '..' in filename: + abort(400) + upload_dir = current_app.config['UPLOAD_FOLDER'] + return send_from_directory(upload_dir, filename, as_attachment=False) diff --git a/app/templates/admin/settings.html b/app/templates/admin/settings.html index cb47dcf..1f61506 100644 --- a/app/templates/admin/settings.html +++ b/app/templates/admin/settings.html @@ -1,11 +1,11 @@ {% extends "base.html" %} -{% block title %}System Settings — TechDesk{% endblock %} +{% block title %}System Settings — {{ branding.app_name }}{% endblock %} {% block content %} @@ -18,7 +18,103 @@ {% endfor %} {% endwith %} -
+ +
+
+
+ Branding & Appearance +
+
+
+
+ + + + + +
+
+ {% if branding_settings.logo_stored_name %} + Logo + {% else %} + + {{ branding_settings.logo_initials[:2] }} + + {% endif %} +
+
+ + + {% if branding_settings.logo_stored_name %} +
+ +
+ {% endif %} +
+
+ +
+
+ + +
Shown in the sidebar and browser tab.
+
+
+ + +
Shown below the app name in the sidebar.
+
+
+ + +
Shown on the login and register pages.
+
+
+ + +
Fallback when no logo is uploaded.
+
+
+ +
+ + +
+
Accent colour across the interface.
+
+
+ + +
+
+
+ + +
User Registration @@ -27,13 +123,11 @@

When disabled, the /auth/register endpoint redirects all visitors to the login - page with an informational message. Existing accounts and admin-created accounts are - unaffected. + page. Existing accounts and admin-created accounts are unaffected.

-
- +
@@ -66,4 +160,44 @@
+ + {% endblock %} diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html index 9391a39..a97ba75 100644 --- a/app/templates/auth/login.html +++ b/app/templates/auth/login.html @@ -3,7 +3,8 @@ - Login — TechDesk + Login — {{ branding.app_name }} + @@ -41,9 +42,17 @@
- -

TechDesk

-

IT Helpdesk Portal — Sign in to continue

+ {% if branding.logo_stored_name %} + {{ branding.app_name }} + {% else %} + + {% endif %} +

{{ branding.app_name }}

+

+ {% if branding.company_name %}{{ branding.company_name }} — {% endif %}Sign in to continue +

{% with messages = get_flashed_messages(with_categories=true) %} diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html index 1b92bf2..4608592 100644 --- a/app/templates/auth/register.html +++ b/app/templates/auth/register.html @@ -2,7 +2,8 @@ - Register — TechDesk + Register — {{ branding.app_name }} + @@ -282,10 +283,16 @@