Development
This commit is contained in:
parent
5b1f905ed0
commit
44261e5b5c
6 changed files with 87 additions and 33 deletions
|
|
@ -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. '
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue