diff --git a/docker/routlin-dash/app/config_utils.py b/docker/routlin-dash/app/config_utils.py index a20cd04..8fb2fcf 100644 --- a/docker/routlin-dash/app/config_utils.py +++ b/docker/routlin-dash/app/config_utils.py @@ -51,7 +51,7 @@ def init_accounts_db(): cookie_unique_token TEXT PRIMARY KEY, email TEXT UNIQUE, hashed_password TEXT, - tz_offset_seconds INTEGER, + timezone TEXT, verification_code TEXT, code_sent_ts INTEGER, flashes_json TEXT @@ -59,7 +59,7 @@ def init_accounts_db(): CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, account_id TEXT NOT NULL, - tz_offset_seconds INTEGER NOT NULL, + timezone TEXT NOT NULL DEFAULT '', preferences_json TEXT NOT NULL, flashes_json TEXT, session_started_ts INTEGER NOT NULL, @@ -69,11 +69,16 @@ def init_accounts_db(): ); ''') con.commit() - try: - con.execute('ALTER TABLE accounts ADD COLUMN requested_email TEXT') - con.commit() - except Exception: - pass + for ddl in ( + 'ALTER TABLE accounts ADD COLUMN requested_email TEXT', + 'ALTER TABLE clients ADD COLUMN timezone TEXT', + 'ALTER TABLE sessions ADD COLUMN timezone TEXT NOT NULL DEFAULT \'\'', + ): + try: + con.execute(ddl) + con.commit() + except Exception: + pass con.close() _LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'} diff --git a/docker/routlin-dash/app/pages/accountcreate/action.py b/docker/routlin-dash/app/pages/accountcreate/action.py index a61b53f..4dba101 100644 --- a/docker/routlin-dash/app/pages/accountcreate/action.py +++ b/docker/routlin-dash/app/pages/accountcreate/action.py @@ -43,16 +43,6 @@ def _send_verification_email(to_address, code): smtp.send_message(msg) -def _tz_to_offset_seconds(tz_str): - try: - from zoneinfo import ZoneInfo - from datetime import datetime - return int(datetime.now(ZoneInfo(tz_str)).utcoffset().total_seconds()) - except Exception: - import settings as _s - return _s.get_host_utc_offset() - - @bp.route('/action/accountcreate/form_create', methods=['POST']) @auth.require_level('nothing') def form_create(): @@ -88,8 +78,7 @@ def form_create(): salt = bcrypt.gensalt() hashed = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8') - code = f'{secrets.randbelow(1000000):06d}' - tz_offset = _tz_to_offset_seconds(tz) + code = f'{secrets.randbelow(1000000):06d}' try: _send_verification_email(account['email_address'], code) @@ -101,15 +90,15 @@ def form_create(): con = config_utils.open_accounts_db() con.execute( '''INSERT INTO clients - (cookie_unique_token, email, hashed_password, tz_offset_seconds, verification_code, code_sent_ts) + (cookie_unique_token, email, hashed_password, timezone, verification_code, code_sent_ts) VALUES (?,?,?,?,?,?) ON CONFLICT(cookie_unique_token) DO UPDATE SET email=excluded.email, hashed_password=excluded.hashed_password, - tz_offset_seconds=excluded.tz_offset_seconds, + timezone=excluded.timezone, verification_code=excluded.verification_code, code_sent_ts=excluded.code_sent_ts''', - (session.sid, account['email_address'].lower(), hashed, tz_offset, code, int(time.time())) + (session.sid, account['email_address'].lower(), hashed, tz, code, int(time.time())) ) con.commit() con.close() diff --git a/docker/routlin-dash/app/pages/accountlogin/action.py b/docker/routlin-dash/app/pages/accountlogin/action.py index c2f4c23..b402b9d 100644 --- a/docker/routlin-dash/app/pages/accountlogin/action.py +++ b/docker/routlin-dash/app/pages/accountlogin/action.py @@ -67,7 +67,6 @@ def form_login(): session.clear() session['account_id'] = account['account_id'] - session['tz_offset_seconds'] = settings.get_host_utc_offset() session['timezone'] = settings.get_host_timezone() session['apply_changes_immediately'] = False session.permanent = True diff --git a/docker/routlin-dash/app/pages/accountverifyemail/action.py b/docker/routlin-dash/app/pages/accountverifyemail/action.py index e677f63..46fca4c 100644 --- a/docker/routlin-dash/app/pages/accountverifyemail/action.py +++ b/docker/routlin-dash/app/pages/accountverifyemail/action.py @@ -36,7 +36,7 @@ def email_verify(): con = config_utils.open_accounts_db() con.execute( '''UPDATE clients SET email=NULL, hashed_password=NULL, - tz_offset_seconds=NULL, verification_code=NULL, code_sent_ts=NULL + timezone=NULL, verification_code=NULL, code_sent_ts=NULL WHERE cookie_unique_token=?''', (token,) ) @@ -71,7 +71,7 @@ def email_verify(): ) con.execute( '''UPDATE clients SET email=NULL, hashed_password=NULL, - tz_offset_seconds=NULL, verification_code=NULL, code_sent_ts=NULL + timezone=NULL, verification_code=NULL, code_sent_ts=NULL WHERE cookie_unique_token=?''', (token,) ) @@ -82,7 +82,7 @@ def email_verify(): return redirect(f'/{_PAGE}') session['account_id'] = account['account_id'] - session['tz_offset_seconds'] = int(client['tz_offset_seconds']) + session['timezone'] = str(client['timezone'] or '') session['apply_changes_immediately'] = False session.permanent = True diff --git a/docker/routlin-dash/app/pages/preferences/action.py b/docker/routlin-dash/app/pages/preferences/action.py index b680bce..9a334ea 100644 --- a/docker/routlin-dash/app/pages/preferences/action.py +++ b/docker/routlin-dash/app/pages/preferences/action.py @@ -10,16 +10,6 @@ _PAGE = Path(__file__).parent.name bp = Blueprint(_PAGE, __name__) -def _tz_to_offset_seconds(tz_str): - try: - from zoneinfo import ZoneInfo - from datetime import datetime - return int(datetime.now(ZoneInfo(tz_str)).utcoffset().total_seconds()) - except Exception: - import settings as _s - return _s.get_host_utc_offset() - - @bp.route('/action/preferences/accountdetails_save', methods=['POST']) @auth.require_level('viewer') def accountdetails_save(): @@ -29,9 +19,7 @@ def accountdetails_save(): flash('Timezone is required.', 'error') return redirect(f'/{_PAGE}') - tz_offset = _tz_to_offset_seconds(tz) session['timezone'] = tz - session['tz_offset_seconds'] = tz_offset flash('Preferences saved.', 'success') return redirect(f'/{_PAGE}') diff --git a/docker/routlin-dash/app/session_interface.py b/docker/routlin-dash/app/session_interface.py index 6130c88..c730e45 100644 --- a/docker/routlin-dash/app/session_interface.py +++ b/docker/routlin-dash/app/session_interface.py @@ -4,6 +4,7 @@ import time import uuid from flask.sessions import SessionInterface, SessionMixin from werkzeug.datastructures import CallbackDict +import settings as _settings _LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'} @@ -36,7 +37,7 @@ class SqliteSessionInterface(SessionInterface): try: con = self._connect() row = con.execute( - '''SELECT s.session_id, s.account_id, s.tz_offset_seconds, + '''SELECT s.session_id, s.account_id, s.timezone, s.preferences_json, s.flashes_json, a.email, a.access_level FROM sessions s @@ -47,12 +48,12 @@ class SqliteSessionInterface(SessionInterface): if row: prefs = json.loads(row['preferences_json'] or '{}') flashes = json.loads(row['flashes_json'] or '[]') + tz = str(row['timezone'] or '') or _settings.get_host_timezone() data = { 'account_id': str(row['account_id']), 'email_address': str(row['email']), 'access_level': _LEVEL_INT_TO_STR.get(row['access_level'], 'viewer'), - 'tz_offset_seconds': int(row['tz_offset_seconds']), - 'timezone': str(prefs.get('timezone', '')), + 'timezone': tz, 'apply_changes_immediately': bool(prefs.get('apply_changes_immediately', False)), '_flashes': flashes, '_permanent': True, @@ -93,24 +94,23 @@ class SqliteSessionInterface(SessionInterface): try: con = self._connect() if account_id: - prefs = json.dumps({ - 'timezone': session.get('timezone', ''), + prefs = json.dumps({ 'apply_changes_immediately': bool(session.get('apply_changes_immediately', False)), }) - tz_offset = int(session.get('tz_offset_seconds', 0)) + tz = session.get('timezone', '') con.execute('INSERT OR IGNORE INTO clients (cookie_unique_token) VALUES (?)', (session.sid,)) con.execute( '''INSERT INTO sessions - (session_id, account_id, tz_offset_seconds, preferences_json, + (session_id, account_id, timezone, preferences_json, flashes_json, session_started_ts, last_seen_ts) VALUES (?,?,?,?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET account_id=excluded.account_id, - tz_offset_seconds=excluded.tz_offset_seconds, + timezone=excluded.timezone, preferences_json=excluded.preferences_json, flashes_json=excluded.flashes_json, last_seen_ts=excluded.last_seen_ts''', - (session.sid, account_id, tz_offset, prefs, flashes_json, now, now) + (session.sid, account_id, tz, prefs, flashes_json, now, now) ) else: con.execute( diff --git a/docker/routlin-dash/app/settings.py b/docker/routlin-dash/app/settings.py index 161b219..22482c8 100644 --- a/docker/routlin-dash/app/settings.py +++ b/docker/routlin-dash/app/settings.py @@ -31,6 +31,16 @@ def get_host_utc_offset(): return time.localtime().tm_gmtoff +def get_client_utc_offset(timezone_str): + """Return signed integer UTC offset in seconds for the given IANA timezone string.""" + try: + from zoneinfo import ZoneInfo + from datetime import datetime + return int(datetime.now(ZoneInfo(timezone_str)).utcoffset().total_seconds()) + except Exception: + return get_host_utc_offset() + + def get_host_timezone(): """Return the host timezone name (e.g. 'America/New_York'), or '' if unknown.""" tz = os.environ.get('TZ', '').strip()