Development

This commit is contained in:
Matthew Grotke 2026-06-13 00:03:11 -04:00
parent 5b1f905ed0
commit 44261e5b5c
6 changed files with 87 additions and 33 deletions

View file

@ -152,7 +152,7 @@ app.register_blueprint(api_apply_health_bp)
def _seed_initial_account(): def _seed_initial_account():
import uuid as _uuid, time as _t 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 email:
if not config_utils.list_accounts(): if not config_utils.list_accounts():
print('[main] WARNING: No accounts exist and INITIAL_MANAGER_EMAIL is not set. ' print('[main] WARNING: No accounts exist and INITIAL_MANAGER_EMAIL is not set. '

View file

@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
from flask import Blueprint, request, session, redirect, flash from flask import Blueprint, request, session, redirect, flash
import os, bcrypt, secrets, smtplib import bcrypt, secrets, smtplib
import time import time
from email.message import EmailMessage from email.message import EmailMessage
import auth import auth
@ -15,14 +15,16 @@ CODE_TTL_SECS = 15 * 60
def _send_verification_email(to_address, code): def _send_verification_email(to_address, code):
host = os.environ.get('SMTP_HOST', '') import settings as _s
port = int(os.environ.get('SMTP_PORT', 587)) smtp = _s.get_smtp_config()
user = os.environ.get('SMTP_USER', '') host = smtp['host']
password = os.environ.get('SMTP_PASSWORD', '') port = smtp['port']
from_addr = os.environ.get('SMTP_FROM', user) user = smtp['user']
password = smtp['password']
from_addr = smtp['from'] or user
if not host: if not host:
raise RuntimeError('SMTP_HOST is not configured.') raise RuntimeError('SMTP host is not configured.')
msg = EmailMessage() msg = EmailMessage()
msg['Subject'] = f'{config_utils.WEB_APP_DISPLAY_NAME} - Email Verification' msg['Subject'] = f'{config_utils.WEB_APP_DISPLAY_NAME} - Email Verification'

View file

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
from flask import Blueprint, request, session, redirect, flash from flask import Blueprint, request, session, redirect, flash
import os, re, secrets, sqlite3, time import os, re, secrets, sqlite3, time
import settings
from datetime import datetime, timezone from datetime import datetime, timezone
import auth import auth
import config_utils import config_utils
@ -215,7 +216,7 @@ def accounts_delete():
target = accounts[row_index] target = accounts[row_index]
target_email = target.get('email_address', '').lower() target_email = target.get('email_address', '').lower()
current_email = session.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: if target_email == current_email and target_email != initial_email:
flash('You cannot remove your own account.', 'error') flash('You cannot remove your own account.', 'error')

View file

@ -1,5 +1,24 @@
import json
import os 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(): def product_name():
return os.environ.get('PRODUCT_NAME', 'routlin') return os.environ.get('PRODUCT_NAME', 'routlin')
@ -54,14 +73,34 @@ def get_host_timezone():
return '' 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(): def get_credentials_key():
"""Return a Fernet-compatible key derived from the CREDENTIALS_KEY environment variable, """Return a Fernet-compatible key derived from the credentials_key in app_config.json
or None if not set. SHA-256 hashes the raw string to produce 32 bytes, which are then (or CREDENTIALS_KEY env var as fallback), or None if not set. SHA-256 hashes the raw
URL-safe base64-encoded as required by Fernet.""" string to produce 32 bytes, URL-safe base64-encoded as required by Fernet."""
import base64 import base64
import hashlib 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: if not key_str:
return None return None
raw = hashlib.sha256(key_str.encode()).digest() raw = hashlib.sha256(key_str.encode()).digest()
return base64.urlsafe_b64encode(raw) 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(),
}

View file

@ -17,13 +17,6 @@ services:
- /var/log/freeradius:/var/log/freeradius - /var/log/freeradius:/var/log/freeradius
environment: environment:
- PYTHONPATH=/routlin_location - 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 - DEV_MODE=true
user: "${UID:-1000}:${GID:-1000}" user: "${UID:-1000}:${GID:-1000}"
restart: unless-stopped restart: unless-stopped

View file

@ -11,6 +11,7 @@ Usage:
""" """
import argparse import argparse
import json
import os import os
import re import re
import shutil import shutil
@ -40,6 +41,7 @@ HEALTH_FILE = SCRIPT_DIR / ".health"
SNAPSHOTS_DIR = SCRIPT_DIR / ".snapshots" SNAPSHOTS_DIR = SCRIPT_DIR / ".snapshots"
CAPTIVE_QUEUE_FILE = SCRIPT_DIR / ".captive-queue" CAPTIVE_QUEUE_FILE = SCRIPT_DIR / ".captive-queue"
DASH_DATA_DIR = COMPOSE_FILE.parent / "data" DASH_DATA_DIR = COMPOSE_FILE.parent / "data"
APP_CONFIG_FILE = DASH_DATA_DIR / "app_config.json"
# Dashboard systemd timer # Dashboard systemd timer
DASHB_TIMER_NAME = f"{PRODUCT_NAME}-dashboard-queue" DASHB_TIMER_NAME = f"{PRODUCT_NAME}-dashboard-queue"
@ -286,9 +288,14 @@ def _set_env_var(content, key, value):
def _dash_already_configured(): def _dash_already_configured():
if not COMPOSE_FILE.exists(): if not APP_CONFIG_FILE.exists():
return False 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): def setup_docker_compose(reuse_config=False):
header("Dashboard Configuration") header("Dashboard Configuration")
@ -320,8 +327,6 @@ def setup_docker_compose(reuse_config=False):
print(" Dashboard container started.") print(" Dashboard container started.")
return return
content = COMPOSE_FILE.read_text()
print() print()
print(" SMTP is used to send email verification codes for new accounts.") print(" SMTP is used to send email verification codes for new accounts.")
print(" (Gmail users: use an App Password, not your account password.)") 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.") print(" Please enter a valid email address.")
manager_email = prompt_str("Initial manager account email") 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_host = prompt_str("SMTP host", default="smtp.gmail.com")
smtp_port = prompt_str("SMTP port", default="587") smtp_port = prompt_str("SMTP port", default="587")
smtp_user = prompt_str("SMTP username (email)") smtp_user = prompt_str("SMTP username (email)")
smtp_password = prompt_str("SMTP password", secret=True) smtp_password = prompt_str("SMTP password", secret=True)
smtp_from = prompt_str("SMTP From address", default=smtp_user) smtp_from = prompt_str("SMTP From address", default=smtp_user)
content = _set_env_var(content, "INITIAL_MANAGER_EMAIL", manager_email) app_config = {
content = _set_env_var(content, "SMTP_HOST", smtp_host) "initial_manager_email": manager_email,
content = _set_env_var(content, "SMTP_PORT", smtp_port) "credentials_key": credentials_key,
content = _set_env_var(content, "SMTP_USER", smtp_user) "smtp": {
content = _set_env_var(content, "SMTP_PASSWORD", smtp_password) "host": smtp_host,
content = _set_env_var(content, "SMTP_FROM", smtp_from) "port": int(smtp_port),
"user": smtp_user,
"password": smtp_password,
"from": smtp_from,
},
}
COMPOSE_FILE.write_text(content) APP_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
print(f"\n Written: {COMPOSE_FILE}") APP_CONFIG_FILE.write_text(json.dumps(app_config, indent=2) + "\n")
print(f"\n Written: {APP_CONFIG_FILE}")
env = _compose_env() env = _compose_env()
print("\n Stopping existing container...") print("\n Stopping existing container...")
@ -675,7 +694,7 @@ def main():
reuse_config = False reuse_config = False
if dash_installed: if dash_installed:
reuse_config = prompt_yn( 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" default="y"
) )