Development
This commit is contained in:
parent
44261e5b5c
commit
8a8e947fcf
9 changed files with 289 additions and 33 deletions
|
|
@ -9,7 +9,7 @@ APP_DIR = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
CONFIGS_DIR = _settings.routlin_location()
|
CONFIGS_DIR = _settings.routlin_location()
|
||||||
DATA_DIR = '/data'
|
DATA_DIR = '/data'
|
||||||
WWW_DIR = '/www'
|
WWW_DIR = '/www'
|
||||||
ACCOUNTS_DB = f'{DATA_DIR}/.dashboard-accounts'
|
ACCOUNTS_DB = f'{DATA_DIR}/.dashboard-singleaccount' if _settings.is_single_user() else f'{DATA_DIR}/.dashboard-accounts'
|
||||||
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
||||||
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
||||||
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
||||||
|
|
@ -81,6 +81,23 @@ def init_accounts_db():
|
||||||
pass
|
pass
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
|
def init_single_user_session_db():
|
||||||
|
con = _sqlite3.connect(ACCOUNTS_DB, timeout=5)
|
||||||
|
con.execute('PRAGMA journal_mode=WAL')
|
||||||
|
con.executescript('''
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
session_id TEXT PRIMARY KEY,
|
||||||
|
logged_in INTEGER NOT NULL DEFAULT 0,
|
||||||
|
timezone TEXT NOT NULL DEFAULT '',
|
||||||
|
preferences_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
flashes_json TEXT,
|
||||||
|
session_started_ts INTEGER NOT NULL,
|
||||||
|
last_seen_ts INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
''')
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
_LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'}
|
_LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'}
|
||||||
_LEVEL_STR_TO_INT = {'nothing': 0, 'viewer': 1, 'administrator': 2, 'manager': 3}
|
_LEVEL_STR_TO_INT = {'nothing': 0, 'viewer': 1, 'administrator': 2, 'manager': 3}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,13 @@ from api_apply_health import bp as api_apply_health_bp
|
||||||
from session_interface import SqliteSessionInterface
|
from session_interface import SqliteSessionInterface
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.session_interface = SqliteSessionInterface(config_utils.ACCOUNTS_DB)
|
if settings.is_single_user():
|
||||||
config_utils.init_accounts_db()
|
from session_interface import SingleUserSessionInterface
|
||||||
|
config_utils.init_single_user_session_db()
|
||||||
|
app.session_interface = SingleUserSessionInterface(config_utils.ACCOUNTS_DB)
|
||||||
|
else:
|
||||||
|
app.session_interface = SqliteSessionInterface(config_utils.ACCOUNTS_DB)
|
||||||
|
config_utils.init_accounts_db()
|
||||||
|
|
||||||
# Static www/ serving =================================================
|
# Static www/ serving =================================================
|
||||||
|
|
||||||
|
|
@ -83,6 +88,9 @@ def serve_view(page_name):
|
||||||
if not factory.passes(view_req, level):
|
if not factory.passes(view_req, level):
|
||||||
return redirect('/overview' if level > 0 else '/accountlogin')
|
return redirect('/overview' if level > 0 else '/accountlogin')
|
||||||
|
|
||||||
|
if settings.is_single_user() and page_name in ('accountmanage', 'accountcreate', 'accountverifyemail'):
|
||||||
|
return redirect('/overview' if level > 0 else '/accountlogin')
|
||||||
|
|
||||||
cfg = config_utils.load_config()
|
cfg = config_utils.load_config()
|
||||||
|
|
||||||
if level >= factory.LEVEL_RANK['administrator']:
|
if level >= factory.LEVEL_RANK['administrator']:
|
||||||
|
|
@ -138,10 +146,11 @@ app.register_blueprint(physicalinterfaces_bp)
|
||||||
app.register_blueprint(portforwarding_bp)
|
app.register_blueprint(portforwarding_bp)
|
||||||
app.register_blueprint(portwrangling_bp)
|
app.register_blueprint(portwrangling_bp)
|
||||||
app.register_blueprint(preferences_bp)
|
app.register_blueprint(preferences_bp)
|
||||||
app.register_blueprint(accountverifyemail_bp)
|
if not settings.is_single_user():
|
||||||
|
app.register_blueprint(accountverifyemail_bp)
|
||||||
|
app.register_blueprint(accountcreate_bp)
|
||||||
|
app.register_blueprint(accountmanage_bp)
|
||||||
app.register_blueprint(vpn_bp)
|
app.register_blueprint(vpn_bp)
|
||||||
app.register_blueprint(accountcreate_bp)
|
|
||||||
app.register_blueprint(accountmanage_bp)
|
|
||||||
app.register_blueprint(accountlogout_bp)
|
app.register_blueprint(accountlogout_bp)
|
||||||
app.register_blueprint(mdns_bp)
|
app.register_blueprint(mdns_bp)
|
||||||
app.register_blueprint(radius_bp)
|
app.register_blueprint(radius_bp)
|
||||||
|
|
@ -151,6 +160,8 @@ app.register_blueprint(api_apply_health_bp)
|
||||||
|
|
||||||
|
|
||||||
def _seed_initial_account():
|
def _seed_initial_account():
|
||||||
|
if settings.is_single_user():
|
||||||
|
return
|
||||||
import uuid as _uuid, time as _t
|
import uuid as _uuid, time as _t
|
||||||
email = settings.get_initial_manager_email()
|
email = settings.get_initial_manager_email()
|
||||||
if not email:
|
if not email:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,21 @@ def form_login():
|
||||||
flash('Email address and password are required.', 'error')
|
flash('Email address and password are required.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
if settings.is_single_user():
|
||||||
|
stored_hash = settings.get_initial_manager_password_hash()
|
||||||
|
if email != settings.get_initial_manager_email() or not stored_hash:
|
||||||
|
flash('Invalid email address or password.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
if not bcrypt.checkpw(password.encode('utf-8'), stored_hash.encode('utf-8')):
|
||||||
|
flash('Invalid email address or password.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
session.clear()
|
||||||
|
session['logged_in'] = True
|
||||||
|
session['timezone'] = settings.get_host_timezone()
|
||||||
|
session['apply_changes_immediately'] = False
|
||||||
|
session.permanent = True
|
||||||
|
return redirect('/overview')
|
||||||
|
|
||||||
account = config_utils.get_account_by_email(email)
|
account = config_utils.get_account_by_email(email)
|
||||||
|
|
||||||
if account is None:
|
if account is None:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import bcrypt
|
||||||
import auth
|
import auth
|
||||||
import config_utils
|
import config_utils
|
||||||
import sanitize
|
import sanitize
|
||||||
|
import settings
|
||||||
|
|
||||||
_PAGE = Path(__file__).parent.name
|
_PAGE = Path(__file__).parent.name
|
||||||
|
|
||||||
|
|
@ -25,9 +26,62 @@ def accountdetails_save():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/preferences/email_change_direct', methods=['POST'])
|
||||||
|
@auth.require_level('viewer')
|
||||||
|
def email_change_direct():
|
||||||
|
if session.get('email_address', '').lower() != settings.get_initial_manager_email():
|
||||||
|
flash('Not authorised.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
new_email = sanitize.email(request.form.get('new_email', '').strip())
|
||||||
|
if not new_email:
|
||||||
|
flash('A valid email address is required.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
current_email = session.get('email_address', '').lower()
|
||||||
|
if new_email == current_email:
|
||||||
|
flash('That is already your current email address.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
if not settings.is_single_user() and config_utils.get_account_by_email(new_email):
|
||||||
|
flash('That email address is already in use.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
if not settings.is_single_user():
|
||||||
|
try:
|
||||||
|
con = config_utils.open_accounts_db()
|
||||||
|
con.execute(
|
||||||
|
'UPDATE accounts SET email=?, requested_email=NULL WHERE account_id=?',
|
||||||
|
(new_email, session.get('account_id', ''))
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f'Could not update account: {exc}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json as _json
|
||||||
|
cfg_path = Path(settings._APP_CONFIG_PATH)
|
||||||
|
cfg = _json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
||||||
|
cfg['initial_manager_email'] = new_email
|
||||||
|
cfg_path.write_text(_json.dumps(cfg, indent=2) + '\n')
|
||||||
|
settings._app_config_cache = None
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f'Could not update app_config.json: {exc}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
session['email_address'] = new_email
|
||||||
|
flash('Email address updated.', 'success')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/preferences/email_change_request', methods=['POST'])
|
@bp.route('/action/preferences/email_change_request', methods=['POST'])
|
||||||
@auth.require_level('viewer')
|
@auth.require_level('viewer')
|
||||||
def email_change_request():
|
def email_change_request():
|
||||||
|
if settings.is_single_user():
|
||||||
|
flash('Not available in single-user mode.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
new_email = sanitize.email(request.form.get('new_email', '').strip())
|
new_email = sanitize.email(request.form.get('new_email', '').strip())
|
||||||
if not new_email:
|
if not new_email:
|
||||||
flash('A valid email address is required.', 'error')
|
flash('A valid email address is required.', 'error')
|
||||||
|
|
@ -77,6 +131,26 @@ def changepassword_save():
|
||||||
flash('New password must be at least 8 characters.', 'error')
|
flash('New password must be at least 8 characters.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
hashed = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
|
if settings.is_single_user():
|
||||||
|
stored_hash = settings.get_initial_manager_password_hash()
|
||||||
|
if not stored_hash or not bcrypt.checkpw(current_password.encode('utf-8'), stored_hash.encode('utf-8')):
|
||||||
|
flash('Current password is incorrect.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
try:
|
||||||
|
import json as _json
|
||||||
|
cfg_path = Path(settings._APP_CONFIG_PATH)
|
||||||
|
cfg = _json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
||||||
|
cfg['initial_manager_password'] = hashed
|
||||||
|
cfg_path.write_text(_json.dumps(cfg, indent=2) + '\n')
|
||||||
|
settings._app_config_cache = None
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f'Could not update password: {exc}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
flash('Password changed successfully.', 'success')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
account = config_utils.get_account_by_id(session.get('account_id', ''))
|
account = config_utils.get_account_by_id(session.get('account_id', ''))
|
||||||
if account is None:
|
if account is None:
|
||||||
flash('Account not found. Please log in again.', 'error')
|
flash('Account not found. Please log in again.', 'error')
|
||||||
|
|
@ -86,8 +160,6 @@ def changepassword_save():
|
||||||
flash('Current password is incorrect.', 'error')
|
flash('Current password is incorrect.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
hashed = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
con.execute(
|
con.execute(
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"action": "/action/preferences/email_change_request",
|
"action": "%EMAIL_CHANGE_ACTION%",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"text": "Submit Request"
|
"text": "%EMAIL_CHANGE_BTN_TEXT%"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from flask import session
|
||||||
import sanitize
|
import sanitize
|
||||||
import config_utils
|
import config_utils
|
||||||
import factory
|
import factory
|
||||||
|
import settings
|
||||||
|
|
||||||
|
|
||||||
def collect_tokens(cfg):
|
def collect_tokens(cfg):
|
||||||
|
|
@ -12,8 +13,19 @@ def collect_tokens(cfg):
|
||||||
tokens['PREF_TIMEZONE'] = session.get('timezone', '')
|
tokens['PREF_TIMEZONE'] = session.get('timezone', '')
|
||||||
tokens['TIMEZONE_OPTIONS'] = json.dumps(blank + [{'value': tz, 'label': tz} for tz in sanitize.VALID_TIMEZONES])
|
tokens['TIMEZONE_OPTIONS'] = json.dumps(blank + [{'value': tz, 'label': tz} for tz in sanitize.VALID_TIMEZONES])
|
||||||
|
|
||||||
account = config_utils.get_account_by_id(session.get('account_id', ''))
|
is_initial_manager = session.get('email_address', '').lower() == settings.get_initial_manager_email()
|
||||||
requested = (account or {}).get('requested_email', '')
|
if is_initial_manager:
|
||||||
|
tokens['EMAIL_CHANGE_ACTION'] = '/action/preferences/email_change_direct'
|
||||||
|
tokens['EMAIL_CHANGE_BTN_TEXT'] = 'Change Email'
|
||||||
|
else:
|
||||||
|
tokens['EMAIL_CHANGE_ACTION'] = '/action/preferences/email_change_request'
|
||||||
|
tokens['EMAIL_CHANGE_BTN_TEXT'] = 'Submit Request'
|
||||||
|
|
||||||
|
if not settings.is_single_user():
|
||||||
|
account = config_utils.get_account_by_id(session.get('account_id', ''))
|
||||||
|
requested = (account or {}).get('requested_email', '')
|
||||||
|
else:
|
||||||
|
requested = ''
|
||||||
if requested:
|
if requested:
|
||||||
tokens['PENDING_EMAIL_BAR'] = (
|
tokens['PENDING_EMAIL_BAR'] = (
|
||||||
f'<div class="info-bar info-bar-inline info-bar-warning">'
|
f'<div class="info-bar info-bar-inline info-bar-warning">'
|
||||||
|
|
|
||||||
|
|
@ -134,3 +134,97 @@ class SqliteSessionInterface(SessionInterface):
|
||||||
secure=self.get_cookie_secure(app),
|
secure=self.get_cookie_secure(app),
|
||||||
samesite=self.get_cookie_samesite(app),
|
samesite=self.get_cookie_samesite(app),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SingleUserSessionInterface(SessionInterface):
|
||||||
|
def __init__(self, db_path):
|
||||||
|
self.db_path = db_path
|
||||||
|
|
||||||
|
def _connect(self):
|
||||||
|
con = sqlite3.connect(self.db_path, timeout=5)
|
||||||
|
con.execute('PRAGMA journal_mode=WAL')
|
||||||
|
con.row_factory = sqlite3.Row
|
||||||
|
return con
|
||||||
|
|
||||||
|
def open_session(self, app, request):
|
||||||
|
name = app.config.get('SESSION_COOKIE_NAME', 'session')
|
||||||
|
sid = request.cookies.get(name)
|
||||||
|
if not sid:
|
||||||
|
return SqliteSession(sid=str(uuid.uuid4()), new=True)
|
||||||
|
try:
|
||||||
|
con = self._connect()
|
||||||
|
row = con.execute('SELECT * FROM sessions WHERE session_id=?', (sid,)).fetchone()
|
||||||
|
con.close()
|
||||||
|
if row:
|
||||||
|
prefs = json.loads(row['preferences_json'] or '{}')
|
||||||
|
flashes = json.loads(row['flashes_json'] or '[]')
|
||||||
|
data = {'_flashes': flashes}
|
||||||
|
if row['logged_in']:
|
||||||
|
tz = str(row['timezone'] or '') or _settings.get_host_timezone()
|
||||||
|
data.update({
|
||||||
|
'email_address': _settings.get_initial_manager_email(),
|
||||||
|
'access_level': 'manager',
|
||||||
|
'timezone': tz,
|
||||||
|
'apply_changes_immediately': bool(prefs.get('apply_changes_immediately', False)),
|
||||||
|
'_permanent': True,
|
||||||
|
'logged_in': True,
|
||||||
|
})
|
||||||
|
return SqliteSession(data, sid=sid, new=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return SqliteSession(sid=sid, new=False)
|
||||||
|
|
||||||
|
def save_session(self, app, session, response):
|
||||||
|
name = app.config.get('SESSION_COOKIE_NAME', 'session')
|
||||||
|
domain = self.get_cookie_domain(app)
|
||||||
|
path = self.get_cookie_path(app)
|
||||||
|
|
||||||
|
if not session and session.modified and not session.new:
|
||||||
|
try:
|
||||||
|
con = self._connect()
|
||||||
|
con.execute('DELETE FROM sessions WHERE session_id=?', (session.sid,))
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
response.delete_cookie(name, domain=domain, path=path)
|
||||||
|
return
|
||||||
|
|
||||||
|
prefs = json.dumps({
|
||||||
|
'apply_changes_immediately': bool(session.get('apply_changes_immediately', False)),
|
||||||
|
})
|
||||||
|
flashes_json = json.dumps(session.get('_flashes', []))
|
||||||
|
now = int(time.time())
|
||||||
|
logged_in = 1 if session.get('logged_in') else 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
con = self._connect()
|
||||||
|
con.execute(
|
||||||
|
'''INSERT INTO sessions
|
||||||
|
(session_id, logged_in, timezone, preferences_json,
|
||||||
|
flashes_json, session_started_ts, last_seen_ts)
|
||||||
|
VALUES (?,?,?,?,?,?,?)
|
||||||
|
ON CONFLICT(session_id) DO UPDATE SET
|
||||||
|
logged_in=excluded.logged_in,
|
||||||
|
timezone=excluded.timezone,
|
||||||
|
preferences_json=excluded.preferences_json,
|
||||||
|
flashes_json=excluded.flashes_json,
|
||||||
|
last_seen_ts=excluded.last_seen_ts''',
|
||||||
|
(session.sid, logged_in,
|
||||||
|
session.get('timezone', ''),
|
||||||
|
prefs, flashes_json, now, now)
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
response.set_cookie(
|
||||||
|
name, session.sid,
|
||||||
|
expires=self.get_expiration_time(app, session),
|
||||||
|
httponly=self.get_cookie_httponly(app),
|
||||||
|
domain=domain,
|
||||||
|
path=path,
|
||||||
|
secure=self.get_cookie_secure(app),
|
||||||
|
samesite=self.get_cookie_samesite(app),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,14 @@ def get_initial_manager_email():
|
||||||
return str(cfg.get('initial_manager_email') or os.environ.get('INITIAL_MANAGER_EMAIL', '')).strip().lower()
|
return str(cfg.get('initial_manager_email') or os.environ.get('INITIAL_MANAGER_EMAIL', '')).strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def is_single_user():
|
||||||
|
return 'initial_manager_password' in _load_app_config()
|
||||||
|
|
||||||
|
|
||||||
|
def get_initial_manager_password_hash():
|
||||||
|
return _load_app_config().get('initial_manager_password', '')
|
||||||
|
|
||||||
|
|
||||||
def get_credentials_key():
|
def get_credentials_key():
|
||||||
"""Return a Fernet-compatible key derived from the credentials_key in app_config.json
|
"""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
|
(or CREDENTIALS_KEY env var as fallback), or None if not set. SHA-256 hashes the raw
|
||||||
|
|
|
||||||
|
|
@ -327,11 +327,6 @@ def setup_docker_compose(reuse_config=False):
|
||||||
print(" Dashboard container started.")
|
print(" Dashboard container started.")
|
||||||
return
|
return
|
||||||
|
|
||||||
print()
|
|
||||||
print(" SMTP is used to send email verification codes for new accounts.")
|
|
||||||
print(" (Gmail users: use an App Password, not your account password.)")
|
|
||||||
print()
|
|
||||||
|
|
||||||
manager_email = prompt_str("Initial manager account email")
|
manager_email = prompt_str("Initial manager account email")
|
||||||
while not manager_email or "@" not in manager_email:
|
while not manager_email or "@" not in manager_email:
|
||||||
print(" Please enter a valid email address.")
|
print(" Please enter a valid email address.")
|
||||||
|
|
@ -345,23 +340,55 @@ def setup_docker_compose(reuse_config=False):
|
||||||
credentials_key = _sec.token_urlsafe(48)
|
credentials_key = _sec.token_urlsafe(48)
|
||||||
print(f" Generated key: {credentials_key}")
|
print(f" Generated key: {credentials_key}")
|
||||||
|
|
||||||
smtp_host = prompt_str("SMTP host", default="smtp.gmail.com")
|
print()
|
||||||
smtp_port = prompt_str("SMTP port", default="587")
|
multi_user = prompt_yn(
|
||||||
smtp_user = prompt_str("SMTP username (email)")
|
f"Enable multi-user access? (requires SMTP to send account verification emails)",
|
||||||
smtp_password = prompt_str("SMTP password", secret=True)
|
default="n"
|
||||||
smtp_from = prompt_str("SMTP From address", default=smtp_user)
|
)
|
||||||
|
|
||||||
app_config = {
|
if multi_user:
|
||||||
"initial_manager_email": manager_email,
|
print()
|
||||||
"credentials_key": credentials_key,
|
print(" (Gmail users: use an App Password, not your account password.)")
|
||||||
"smtp": {
|
print()
|
||||||
"host": smtp_host,
|
smtp_host = prompt_str("SMTP host", default="smtp.gmail.com")
|
||||||
"port": int(smtp_port),
|
smtp_port = prompt_str("SMTP port", default="587")
|
||||||
"user": smtp_user,
|
smtp_user = prompt_str("SMTP username (email)")
|
||||||
"password": smtp_password,
|
smtp_password = prompt_str("SMTP password", secret=True)
|
||||||
"from": smtp_from,
|
smtp_from = prompt_str("SMTP From address", default=smtp_user)
|
||||||
},
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print()
|
||||||
|
print(f" Single-user mode: only {manager_email} can log in.")
|
||||||
|
print(f" Password must be at least 8 characters.")
|
||||||
|
print()
|
||||||
|
while True:
|
||||||
|
import getpass as _gp
|
||||||
|
pw = _gp.getpass(f" Password for {manager_email}: ")
|
||||||
|
pw2 = _gp.getpass(f" Confirm password: ")
|
||||||
|
if pw != pw2:
|
||||||
|
print(" Passwords do not match. Try again.")
|
||||||
|
continue
|
||||||
|
if len(pw) < 8:
|
||||||
|
print(" Password must be at least 8 characters.")
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
import bcrypt as _bcrypt
|
||||||
|
pw_hash = _bcrypt.hashpw(pw.encode('utf-8'), _bcrypt.gensalt()).decode('utf-8')
|
||||||
|
app_config = {
|
||||||
|
"initial_manager_email": manager_email,
|
||||||
|
"credentials_key": credentials_key,
|
||||||
|
"initial_manager_password": pw_hash,
|
||||||
|
}
|
||||||
|
|
||||||
APP_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
APP_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
APP_CONFIG_FILE.write_text(json.dumps(app_config, indent=2) + "\n")
|
APP_CONFIG_FILE.write_text(json.dumps(app_config, indent=2) + "\n")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue