Prep admin form for production deployment

Only display Wifi Settings if raspap is installed.
Change_manager is now more standalone and can be run to monitor when settings.txt is last modified.
This commit is contained in:
Ana Custura 2026-03-06 12:26:33 +00:00
parent 0cfd475031
commit 83fd4c4ec5
5 changed files with 106 additions and 69 deletions

View file

@ -1,5 +1,5 @@
from app import app from app import app
from flask import render_template, flash, redirect, url_for, request, session, send_file from flask import render_template, flash, redirect, url_for, send_file
from app.forms import LoginForm, SettingsForm from app.forms import LoginForm, SettingsForm
from flask_login import login_user, current_user, logout_user, login_required from flask_login import login_user, current_user, logout_user, login_required
import sqlalchemy as sa import sqlalchemy as sa
@ -16,7 +16,7 @@ import string
import glob import glob
import time import time
import qrcode import qrcode
from app.change_manager import CHANGES_REQUIRING_RESTART, check_settings from app.change_manager import CHANGES_REQUIRING_RESTART
def gen_username() -> str: def gen_username() -> str:
words = top_n_list("en", 5000) words = top_n_list("en", 5000)
@ -155,6 +155,7 @@ def logout():
@app.route('/admin', methods=['GET', 'POST']) @app.route('/admin', methods=['GET', 'POST'])
@login_required @login_required
def admin(): def admin():
raspap_installed = os.path.exists("/var/www/html/raspap")
form = SettingsForm() form = SettingsForm()
populate_settings = ['butterbox_name', 'wifi_password', 'ssid', 'butterbox_hostname', 'root_account_settings', 'ssh_access_settings'] populate_settings = ['butterbox_name', 'wifi_password', 'ssid', 'butterbox_hostname', 'root_account_settings', 'ssh_access_settings']
bool_settings = ['enable_access_point','enable_file_viewer', 'enable_map_viewer', 'enable_app_store', 'enable_chat', 'enable_deltachat', 'enable_wifi_sharing'] bool_settings = ['enable_access_point','enable_file_viewer', 'enable_map_viewer', 'enable_app_store', 'enable_chat', 'enable_deltachat', 'enable_wifi_sharing']
@ -214,10 +215,9 @@ def admin():
if form.apply_changes.data: if form.apply_changes.data:
set_setting('apply_changes', "true") set_setting('apply_changes', "true")
dump_settings("settings.txt") dump_settings("settings.txt")
check_settings()
flash(_("⚠️ Changes applied! Please wait for the box to restart.")) flash(_("⚠️ Changes applied! Please wait for the box to restart."))
return render_template('admin.html', get_setting=get_setting, form=form) return render_template('admin.html', raspap_installed=raspap_installed, get_setting=get_setting, form=form)
@app.route('/messaging', methods=['GET', 'POST']) @app.route('/messaging', methods=['GET', 'POST'])

View file

@ -4,7 +4,9 @@
.butter-title { .butter-title {
text-align: center; text-align: center;
} }
.butter-form-margin {
margin-right: 10px;
}
.butter-service { .butter-service {
border-radius: 20px; border-radius: 20px;
} }

View file

@ -16,61 +16,90 @@
{{ form.apply_changes(class="button is-warning") }} {{ form.apply_changes(class="button is-warning") }}
{% endif %} {% endif %}
</div> </div>
<div class="field">
{{ wtf.form_input_field(form.ssid, form.ssid.errors) }}
<p class="help"> This is the name of the advertised Wi-Fi network. Current SSID: {{ get_setting('ssid') }}</p> <label class="label is-large">Services</label>
<div class="field checkbox">
{{ wtf.form_bool_field(form.enable_map_viewer) }}
<p class="help butter-form-margin">Whether map services are enabled.</p>
</div> </div>
<div class="password"> <div class="field checkbox">
{{ wtf.form_input_field(form.wifi_password, form.wifi_password.errors) }} {{ wtf.form_bool_field(form.enable_chat) }}
<p class="help"> This is the secret key needed to connect to the Wi-Fi network. By default, this is not set and everyone can join. <p class="help butter-form-margin">Whether Matrix chat services are enabled.</p>
Current password: {{ get_setting('wifi_password') or 'Not set' }}</p>
</div> </div>
<div class="field checkbox">
{{ wtf.form_bool_field(form.enable_deltachat) }}
<p class="help butter-form-margin">Whether messaging using DeltaChat is enabled.</p>
</div>
<div class="field checkbox">
{{ wtf.form_bool_field(form.enable_file_viewer) }}
<p class="help butter-form-margin">Whether files services via USB are enabled.</p>
</div>
<div class="field checkbox">
{{ wtf.form_bool_field(form.enable_app_store) }}
<p class="help">Whether app store services are enabled.</p>
</div>
<hr>
<label class="label is-large">Branding and name</label>
<div class="field"> <div class="field">
{{ wtf.form_input_field(form.butterbox_name, form.butterbox_name.errors) }} {{ wtf.form_input_field(form.butterbox_name, form.butterbox_name.errors) }}
<p class="help">This is the name shown in the UI. <p class="help">This is the name shown in the UI.
Current name: {{ get_setting('butterbox_name') }}, accessed at {{ get_setting('butterbox_name') }}.local.</p> Current name: {{ get_setting('butterbox_name') }}, accessed at {{ get_setting('butterbox_name') }}.local.</p>
</div> </div>
<div class="field"> <div class="field">
{{ wtf.form_input_field(form.butterbox_hostname, form.butterbox_hostname.errors) }} {{ wtf.form_input_field(form.butterbox_hostname, form.butterbox_hostname.errors) }}
<p class="help">This is used to access the box locally by adding .local or .lan in your browser. <p class="help">This is the URL used to access the box by adding .local in your browser.
Current hostname: {{ get_setting('butterbox_hostname') }}.local.</p> Current hostname: {{ get_setting('butterbox_hostname') }}.local.</p>
<div class="checkbox">
{{ wtf.form_bool_field(form.enable_access_point) }}
<p class="help">Whether this box will advertise a Wi-Fi network.</p>
</div>
<div class="checkbox">
{{ wtf.form_bool_field(form.enable_wifi_sharing) }}
<p class="help">Whether a share button for the Wi-Fi network is available.</p>
</div>
<div class="checkbox">
{{ wtf.form_bool_field(form.enable_map_viewer) }}
<p class="help">Whether map services are enabled.</p>
</div>
<div class="checkbox">
{{ wtf.form_bool_field(form.enable_chat) }}
<p class="help">Whether chat services are enabled.</p>
</div>
<div class="checkbox">
{{ wtf.form_bool_field(form.enable_deltachat) }}
<p class="help">Whether secure messaging using DeltaChat is enabled.</p>
</div>
<div class="checkbox">
{{ wtf.form_bool_field(form.enable_file_viewer) }}
<p class="help">Whether files services via USB are enabled.</p>
</div>
<div class="checkbox">
{{ wtf.form_bool_field(form.enable_app_store) }}
<p class="help">Whether app store services are enabled.</p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">{{ form.butterbox_logo.label }} </label>
<div class="control block">{{ form.butterbox_logo(class='label', style="width: 280px") }}</div>
{{ wtf.field_errors(form.butterbox_logo.errors) }}
<div class="block"><p class="help">This is the logo shown in the UI. Current logo:</p></div>
<img src="{{ get_setting('butterbox_logo') }}" style="height: 50px">
</div>
<hr>
<label class="label is-large">Wi-Fi and access point</label>
{% if raspap_installed %}
<div class="field">
{{ wtf.form_input_field(form.ssid, form.ssid.errors) }}
<p class="help"> This is the name of the advertised Wi-Fi network. Current SSID: {{ get_setting('ssid') }}</p>
</div>
<div class="field password">
{{ wtf.form_input_field(form.wifi_password, form.wifi_password.errors) }}
<p class="help"> This is the secret key needed to connect to the Wi-Fi network. By default, this is not set and everyone can join.
Current password: {{ get_setting('wifi_password') or 'Not set' }}</p>
</div>
<div class="field checkbox">
{{ wtf.form_bool_field(form.enable_access_point) }}
<p class="butter-form-margin help">Whether this box will advertise a Wi-Fi network.</p>
</div>
<div class="field checkbox">
{{ wtf.form_bool_field(form.enable_wifi_sharing) }}
<p class="butter-form-margin help">Whether a share button for the Wi-Fi network is available.</p>
</div>
{% else %}
<p> Access point is only enabled when using a Raspberry Pi. </p>
{% endif %}
<hr>
<label class="label is-large">Access and security</label>
<div class="control field">
{{ wtf.form_password_field(form.admin_password, form.admin_password.errors) }} {{ wtf.form_password_field(form.admin_password, form.admin_password.errors) }}
<p class="help">Password for accessing this browser interface.</p> <p class="help">Password for accessing this browser interface.</p>
</div> </div>
<div class="field block"> <div class="control block">
<label class="label">{{ form.root_account_settings.label }} </label> <label class="label">{{ form.root_account_settings.label }} </label>
{% for subfield in form.root_account_settings %} {% for subfield in form.root_account_settings %}
<label class="radio"> <label class="radio butter-form-margin">
{% if get_setting('root_account_settings') == subfield._value() %} {% if get_setting('root_account_settings') == subfield._value() %}
<input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/> <input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/>
{% else %} {% else %}
@ -85,7 +114,7 @@
<div class="control block"> <div class="control block">
<label class="label">{{ form.ssh_access_settings.label }} </label> <label class="label">{{ form.ssh_access_settings.label }} </label>
{% for subfield in form.ssh_access_settings %} {% for subfield in form.ssh_access_settings %}
<label class="radio"> <label class="radio butter-form-margin">
{% if get_setting('ssh_access_settings') == subfield._value() %} {% if get_setting('ssh_access_settings') == subfield._value() %}
<input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/> <input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/>
{% else %} {% else %}
@ -96,13 +125,7 @@
{% endfor %} {% endfor %}
{{ wtf.field_errors(form.ssh_access_settings.errors) }} {{ wtf.field_errors(form.ssh_access_settings.errors) }}
</div> </div>
<div class="field">
<label class="label">{{ form.butterbox_logo.label }} </label>
<div class="control block">{{ form.butterbox_logo(class='label', style="width: 280px") }}</div>
{{ wtf.field_errors(form.butterbox_logo.errors) }}
<div class="block"><p class="help">This is the logo shown in the UI. Current logo:</p></div>
<img src="{{ get_setting('butterbox_logo') }}" style="height: 50px">
</div>
</form> </form>

View file

@ -1,8 +1,9 @@
import os import os
import re import re
import time
from datetime import datetime
from subprocess import run from subprocess import run
import json import json
from app import app
CHANGES_REQUIRING_RESTART = ['wifi_password', 'ssid', 'enable_access_point', 'enable_chat', 'enable_delta_chat', 'butterbox_hostname'] CHANGES_REQUIRING_RESTART = ['wifi_password', 'ssid', 'enable_access_point', 'enable_chat', 'enable_delta_chat', 'butterbox_hostname']
# 'ssh_access_settings', 'root_account_settings'] # 'ssh_access_settings', 'root_account_settings']
@ -15,6 +16,8 @@ def lock_root_account():
def enable_service(service: str): def enable_service(service: str):
is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True) is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True)
if 'not_found' in is_enabled.stdout:
return False
if 'disabled' in is_enabled.stdout: if 'disabled' in is_enabled.stdout:
enable = run(["sudo", "systemctl", "enable", service], capture_output = True, text = True) enable = run(["sudo", "systemctl", "enable", service], capture_output = True, text = True)
if enable.returncode != 0: if enable.returncode != 0:
@ -23,6 +26,8 @@ def enable_service(service: str):
def disable_service(service: str): def disable_service(service: str):
is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True) is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True)
if 'not_found' in is_enabled.stdout:
return False
if 'enabled' in is_enabled.stdout: if 'enabled' in is_enabled.stdout:
result = run(["sudo", "systemctl", "disable", service]) result = run(["sudo", "systemctl", "disable", service])
if result.returncode != 0: if result.returncode != 0:
@ -30,7 +35,7 @@ def disable_service(service: str):
return True return True
def load_setting(setting): def load_setting(setting):
with open("settings.txt", "r") as f: with open("./app/settings.txt", "r") as f:
settings = json.load(f) settings = json.load(f)
return settings[setting] return settings[setting]
@ -61,7 +66,6 @@ def change_line_in_file(target_file: str, regex: str, replacement: str):
else: else:
with open(target_file, "r") as f: with open(target_file, "r") as f:
lines = f.readlines() lines = f.readlines()
print(f"Existing lines are: {lines}")
for i, line in enumerate(lines): for i, line in enumerate(lines):
match = re.fullmatch(replacement, line) match = re.fullmatch(replacement, line)
if match: if match:
@ -71,24 +75,24 @@ def change_line_in_file(target_file: str, regex: str, replacement: str):
match = re.fullmatch(regex, line) match = re.fullmatch(regex, line)
if match: if match:
lines.pop(i) lines.pop(i)
print(f"Found a match at line {i}, {match.string}")
break break
try: try:
lines.append(replacement) lines.append(replacement)
except NameError: except NameError:
raise NameError(f"File {target_file} is empty.") raise NameError(f"File {target_file} is empty.")
print(f"Lines to be written to file are {lines}")
new_lines = "".join(lines) new_lines = "".join(lines)
with open(target_file, "w") as f: with open(target_file, "w") as f:
f.write(new_lines) f.write(new_lines)
def check_settings(): def check_settings(raspap_installed: bool):
print(f"Checking settings...") if not os.path.exists("./settings.txt"):
print(f"App config is {app.config['SETTINGS_CHANGED']}") return
if app.config['SETTINGS_CHANGED']: last_modified = os.path.getmtime('./settings.txt')
diff_in_minutes = (datetime.now().timestamp() - last_modified)/60
if diff_in_minutes < 2:
for s in CHANGES_REQUIRING_RESTART: for s in CHANGES_REQUIRING_RESTART:
if s == "wifi_password": if s == "wifi_password" and raspap_installed:
regex_wpa_method = "wpa=.*?\n" regex_wpa_method = "wpa=.*?\n"
if load_setting("wifi_password") == "": if load_setting("wifi_password") == "":
change_line_in_file("/etc/hostapd/hostapd.conf", regex_wpa_method, f"wpa=none\n") change_line_in_file("/etc/hostapd/hostapd.conf", regex_wpa_method, f"wpa=none\n")
@ -98,25 +102,26 @@ def check_settings():
change_line_in_file("/etc/hostapd/hostapd.conf", regex_pass, change_line_in_file("/etc/hostapd/hostapd.conf", regex_pass,
f"wpa_passphrase={load_setting("wifi_password")}\n") f"wpa_passphrase={load_setting("wifi_password")}\n")
if s == "ssid": if s == "ssid" and raspap_installed:
regex_ssid = "ssid=.*?\n" regex_ssid = "ssid=.*?\n"
change_line_in_file("/etc/hostapd/hostapd.conf", regex_ssid, f"ssid={load_setting("ssid")}\n") change_line_in_file("/etc/hostapd/hostapd.conf", regex_ssid, f"ssid={load_setting("ssid")}\n")
if s == "enable_chat": if s == "enable_chat":
change_service_status("enable_chat", "dendrite") change_service_status("enable_chat", "dendrite")
if s == "enable_access_point": if s == "enable_access_point" and raspap_installed:
change_service_status("enable_access_point", "raspapd") change_service_status("enable_access_point", "raspapd")
if s == "enable_delta_chat": if s == "enable_delta_chat":
change_service_status("enable_delta_chat", "madmail") change_service_status("enable_delta_chat", "madmail")
if s == "butterbox_hostname": if s == "butterbox_hostname":
pass pass
# change in butterbox-dendrite.conf # change in butterbox-dendrite.conf
regex_matrix_server = "server_name:.*?.lan\n" regex_matrix_server = "server_name:.*?.local\n"
change_line_in_file("../dendrite/butterbox-dendrite.conf", regex_matrix_server, change_line_in_file("../dendrite/butterbox-dendrite.conf", regex_matrix_server,
f"server_name: {load_setting("butterbox_hostname")}.lan\n") f"server_name: {load_setting("butterbox_hostname")}.local\n")
# change in butterbox-dnsmasq.conf # change in butterbox-dnsmasq.conf
regex_dns = "address=/.*?.lan/10.3.141.1\n" if raspap_installed:
change_line_in_file("/etc/dnsmasq.d/butterbox-dnsmasq.conf", regex_dns, regex_dns = "address=/.*?.local/10.3.141.1\n"
f"address=/{load_setting("butterbox_hostname")}.lan/10.3.141.1\n") change_line_in_file("/etc/dnsmasq.d/butterbox-dnsmasq.conf", regex_dns,
f"address=/{load_setting("butterbox_hostname")}.local/10.3.141.1\n")
# change in keanu-weblite compiled assets # change in keanu-weblite compiled assets
change_keanu_weblite_config(load_setting("butterbox_hostname")) change_keanu_weblite_config(load_setting("butterbox_hostname"))
if s == "ssh_access_settings": if s == "ssh_access_settings":
@ -140,6 +145,13 @@ def check_settings():
if load_setting("root_account_settings") == "lock_root_account": if load_setting("root_account_settings") == "lock_root_account":
lock_root_account() lock_root_account()
else: else:
# root password implementation here
pass pass
if __name__ == "__main__":
raspap_installed = os.path.exists("/var/www/html/raspapd")
while True:
check_settings(raspap_installed)
time.sleep(30)

View file

@ -24,4 +24,4 @@ class Config:
ENABLE_FILE_VIEWER = "true" ENABLE_FILE_VIEWER = "true"
ENABLE_DELTACHAT = "true" ENABLE_DELTACHAT = "true"
CONVENE_INSTALL_PATH = "/chat" CONVENE_INSTALL_PATH = "/chat"
ENABLE_WIFI_SHARING = "true" ENABLE_WIFI_SHARING = "true" if os.path.exists("/var/www/html/raspap") else "false"