From 44261e5b5c000fe2893982779558970c895e2ace Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sat, 13 Jun 2026 00:03:11 -0400 Subject: [PATCH] Development --- docker/routlin-dash/app/main.py | 2 +- .../app/pages/accountcreate/action.py | 16 ++++--- .../app/pages/accountmanage/action.py | 3 +- docker/routlin-dash/app/settings.py | 47 +++++++++++++++++-- docker/routlin-dash/docker-compose.yml | 7 --- routlin/install.py | 45 +++++++++++++----- 6 files changed, 87 insertions(+), 33 deletions(-) diff --git a/docker/routlin-dash/app/main.py b/docker/routlin-dash/app/main.py index 24b130d..0cddae0 100644 --- a/docker/routlin-dash/app/main.py +++ b/docker/routlin-dash/app/main.py @@ -152,7 +152,7 @@ app.register_blueprint(api_apply_health_bp) def _seed_initial_account(): import uuid as _uuid, time as _t - email = os.environ.get('INITIAL_MANAGER_EMAIL', '').strip().lower() + email = settings.get_initial_manager_email() if not email: if not config_utils.list_accounts(): print('[main] WARNING: No accounts exist and INITIAL_MANAGER_EMAIL is not set. ' diff --git a/docker/routlin-dash/app/pages/accountcreate/action.py b/docker/routlin-dash/app/pages/accountcreate/action.py index 4dba101..edae1eb 100644 --- a/docker/routlin-dash/app/pages/accountcreate/action.py +++ b/docker/routlin-dash/app/pages/accountcreate/action.py @@ -1,6 +1,6 @@ from pathlib import Path from flask import Blueprint, request, session, redirect, flash -import os, bcrypt, secrets, smtplib +import bcrypt, secrets, smtplib import time from email.message import EmailMessage import auth @@ -15,14 +15,16 @@ CODE_TTL_SECS = 15 * 60 def _send_verification_email(to_address, code): - host = os.environ.get('SMTP_HOST', '') - port = int(os.environ.get('SMTP_PORT', 587)) - user = os.environ.get('SMTP_USER', '') - password = os.environ.get('SMTP_PASSWORD', '') - from_addr = os.environ.get('SMTP_FROM', user) + import settings as _s + smtp = _s.get_smtp_config() + host = smtp['host'] + port = smtp['port'] + user = smtp['user'] + password = smtp['password'] + from_addr = smtp['from'] or user if not host: - raise RuntimeError('SMTP_HOST is not configured.') + raise RuntimeError('SMTP host is not configured.') msg = EmailMessage() msg['Subject'] = f'{config_utils.WEB_APP_DISPLAY_NAME} - Email Verification' diff --git a/docker/routlin-dash/app/pages/accountmanage/action.py b/docker/routlin-dash/app/pages/accountmanage/action.py index 7a783e2..e9e7c46 100644 --- a/docker/routlin-dash/app/pages/accountmanage/action.py +++ b/docker/routlin-dash/app/pages/accountmanage/action.py @@ -1,6 +1,7 @@ from pathlib import Path from flask import Blueprint, request, session, redirect, flash import os, re, secrets, sqlite3, time +import settings from datetime import datetime, timezone import auth import config_utils @@ -215,7 +216,7 @@ def accounts_delete(): target = accounts[row_index] target_email = target.get('email_address', '').lower() current_email = session.get('email_address', '').lower() - initial_email = os.environ.get('INITIAL_MANAGER_EMAIL', '').strip().lower() + initial_email = settings.get_initial_manager_email() if target_email == current_email and target_email != initial_email: flash('You cannot remove your own account.', 'error') diff --git a/docker/routlin-dash/app/settings.py b/docker/routlin-dash/app/settings.py index 22482c8..c8a87ae 100644 --- a/docker/routlin-dash/app/settings.py +++ b/docker/routlin-dash/app/settings.py @@ -1,5 +1,24 @@ +import json import os +_APP_CONFIG_PATH = '/data/app_config.json' +_app_config_cache = None +_app_config_mtime = None + + +def _load_app_config(): + global _app_config_cache, _app_config_mtime + try: + mtime = os.path.getmtime(_APP_CONFIG_PATH) + if _app_config_cache is not None and mtime == _app_config_mtime: + return _app_config_cache + with open(_APP_CONFIG_PATH) as f: + _app_config_cache = json.load(f) + _app_config_mtime = mtime + return _app_config_cache + except Exception: + return {} + def product_name(): return os.environ.get('PRODUCT_NAME', 'routlin') @@ -54,14 +73,34 @@ def get_host_timezone(): return '' +def get_initial_manager_email(): + cfg = _load_app_config() + return str(cfg.get('initial_manager_email') or os.environ.get('INITIAL_MANAGER_EMAIL', '')).strip().lower() + + def get_credentials_key(): - """Return a Fernet-compatible key derived from the CREDENTIALS_KEY environment variable, - or None if not set. SHA-256 hashes the raw string to produce 32 bytes, which are then - URL-safe base64-encoded as required by Fernet.""" + """Return a Fernet-compatible key derived from the credentials_key in app_config.json + (or CREDENTIALS_KEY env var as fallback), or None if not set. SHA-256 hashes the raw + string to produce 32 bytes, URL-safe base64-encoded as required by Fernet.""" import base64 import hashlib - key_str = os.environ.get('CREDENTIALS_KEY', '') + cfg = _load_app_config() + key_str = str(cfg.get('credentials_key') or os.environ.get('CREDENTIALS_KEY', '')).strip() if not key_str: return None raw = hashlib.sha256(key_str.encode()).digest() return base64.urlsafe_b64encode(raw) + + +def get_smtp_config(): + """Return SMTP settings from app_config.json, falling back to env vars.""" + cfg = _load_app_config() + smtp = cfg.get('smtp', {}) + user = str(smtp.get('user') or os.environ.get('SMTP_USER', '')).strip() + return { + 'host': str(smtp.get('host') or os.environ.get('SMTP_HOST', '')).strip(), + 'port': int(smtp.get('port') or os.environ.get('SMTP_PORT', 587)), + 'user': user, + 'password': str(smtp.get('password') or os.environ.get('SMTP_PASSWORD', '')).strip(), + 'from': str(smtp.get('from') or os.environ.get('SMTP_FROM', user)).strip(), + } diff --git a/docker/routlin-dash/docker-compose.yml b/docker/routlin-dash/docker-compose.yml index a40ca56..cc5c37f 100644 --- a/docker/routlin-dash/docker-compose.yml +++ b/docker/routlin-dash/docker-compose.yml @@ -17,13 +17,6 @@ services: - /var/log/freeradius:/var/log/freeradius environment: - PYTHONPATH=/routlin_location - - INITIAL_MANAGER_EMAIL=mgrotke@gmail.com - - CREDENTIALS_KEY=TwnRAoORr7OaMVeS3q4JJP3NYvBDlyPB8qgl2ovAlm2OGsNf0qsnv0a67MXgaozKWf5Gc1CM0Z1m0xdTQeiw4R0RKK0fmLKMKfttOp2sfKg9lDsMZavJWzn5VS8dyD - - SMTP_HOST=smtp.gmail.com - - SMTP_PORT=587 - - SMTP_USER=grotek.industries@gmail.com - - SMTP_PASSWORD=lfhrygyuwvlaczaw - - SMTP_FROM=grotek.industries@gmail.com - DEV_MODE=true user: "${UID:-1000}:${GID:-1000}" restart: unless-stopped diff --git a/routlin/install.py b/routlin/install.py index ea4e824..1f9f6bd 100644 --- a/routlin/install.py +++ b/routlin/install.py @@ -11,6 +11,7 @@ Usage: """ import argparse +import json import os import re import shutil @@ -40,6 +41,7 @@ HEALTH_FILE = SCRIPT_DIR / ".health" SNAPSHOTS_DIR = SCRIPT_DIR / ".snapshots" CAPTIVE_QUEUE_FILE = SCRIPT_DIR / ".captive-queue" DASH_DATA_DIR = COMPOSE_FILE.parent / "data" +APP_CONFIG_FILE = DASH_DATA_DIR / "app_config.json" # Dashboard systemd timer DASHB_TIMER_NAME = f"{PRODUCT_NAME}-dashboard-queue" @@ -286,9 +288,14 @@ def _set_env_var(content, key, value): def _dash_already_configured(): - if not COMPOSE_FILE.exists(): + if not APP_CONFIG_FILE.exists(): return False - return bool(re.search(r"^\s*- INITIAL_MANAGER_EMAIL=\S", COMPOSE_FILE.read_text(), re.MULTILINE)) + try: + cfg = json.loads(APP_CONFIG_FILE.read_text()) + return bool(cfg.get('initial_manager_email')) + except Exception: + return False + def setup_docker_compose(reuse_config=False): header("Dashboard Configuration") @@ -320,8 +327,6 @@ def setup_docker_compose(reuse_config=False): print(" Dashboard container started.") return - content = COMPOSE_FILE.read_text() - print() print(" SMTP is used to send email verification codes for new accounts.") print(" (Gmail users: use an App Password, not your account password.)") @@ -332,21 +337,35 @@ def setup_docker_compose(reuse_config=False): print(" Please enter a valid email address.") manager_email = prompt_str("Initial manager account email") + credentials_key = prompt_str( + "Credentials encryption key (press Enter to auto-generate)", default="" + ) + if not credentials_key: + import secrets as _sec + credentials_key = _sec.token_urlsafe(48) + print(f" Generated key: {credentials_key}") + smtp_host = prompt_str("SMTP host", default="smtp.gmail.com") smtp_port = prompt_str("SMTP port", default="587") smtp_user = prompt_str("SMTP username (email)") smtp_password = prompt_str("SMTP password", secret=True) smtp_from = prompt_str("SMTP From address", default=smtp_user) - content = _set_env_var(content, "INITIAL_MANAGER_EMAIL", manager_email) - content = _set_env_var(content, "SMTP_HOST", smtp_host) - content = _set_env_var(content, "SMTP_PORT", smtp_port) - content = _set_env_var(content, "SMTP_USER", smtp_user) - content = _set_env_var(content, "SMTP_PASSWORD", smtp_password) - content = _set_env_var(content, "SMTP_FROM", smtp_from) + app_config = { + "initial_manager_email": manager_email, + "credentials_key": credentials_key, + "smtp": { + "host": smtp_host, + "port": int(smtp_port), + "user": smtp_user, + "password": smtp_password, + "from": smtp_from, + }, + } - COMPOSE_FILE.write_text(content) - print(f"\n Written: {COMPOSE_FILE}") + APP_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + APP_CONFIG_FILE.write_text(json.dumps(app_config, indent=2) + "\n") + print(f"\n Written: {APP_CONFIG_FILE}") env = _compose_env() print("\n Stopping existing container...") @@ -675,7 +694,7 @@ def main(): reuse_config = False if dash_installed: reuse_config = prompt_yn( - "Re-use existing Docker configuration? (Keeps CREDENTIALS_KEY and SMTP credentials)", + "Re-use existing app_config.json? (Keeps credentials key and SMTP settings)", default="y" )