Development
This commit is contained in:
parent
025adb9f15
commit
5b1f905ed0
7 changed files with 38 additions and 47 deletions
|
|
@ -51,7 +51,7 @@ def init_accounts_db():
|
||||||
cookie_unique_token TEXT PRIMARY KEY,
|
cookie_unique_token TEXT PRIMARY KEY,
|
||||||
email TEXT UNIQUE,
|
email TEXT UNIQUE,
|
||||||
hashed_password TEXT,
|
hashed_password TEXT,
|
||||||
tz_offset_seconds INTEGER,
|
timezone TEXT,
|
||||||
verification_code TEXT,
|
verification_code TEXT,
|
||||||
code_sent_ts INTEGER,
|
code_sent_ts INTEGER,
|
||||||
flashes_json TEXT
|
flashes_json TEXT
|
||||||
|
|
@ -59,7 +59,7 @@ def init_accounts_db():
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
session_id TEXT PRIMARY KEY,
|
session_id TEXT PRIMARY KEY,
|
||||||
account_id TEXT NOT NULL,
|
account_id TEXT NOT NULL,
|
||||||
tz_offset_seconds INTEGER NOT NULL,
|
timezone TEXT NOT NULL DEFAULT '',
|
||||||
preferences_json TEXT NOT NULL,
|
preferences_json TEXT NOT NULL,
|
||||||
flashes_json TEXT,
|
flashes_json TEXT,
|
||||||
session_started_ts INTEGER NOT NULL,
|
session_started_ts INTEGER NOT NULL,
|
||||||
|
|
@ -69,8 +69,13 @@ def init_accounts_db():
|
||||||
);
|
);
|
||||||
''')
|
''')
|
||||||
con.commit()
|
con.commit()
|
||||||
|
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:
|
try:
|
||||||
con.execute('ALTER TABLE accounts ADD COLUMN requested_email TEXT')
|
con.execute(ddl)
|
||||||
con.commit()
|
con.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -43,16 +43,6 @@ def _send_verification_email(to_address, code):
|
||||||
smtp.send_message(msg)
|
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'])
|
@bp.route('/action/accountcreate/form_create', methods=['POST'])
|
||||||
@auth.require_level('nothing')
|
@auth.require_level('nothing')
|
||||||
def form_create():
|
def form_create():
|
||||||
|
|
@ -89,7 +79,6 @@ def form_create():
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
hashed = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
hashed = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
||||||
code = f'{secrets.randbelow(1000000):06d}'
|
code = f'{secrets.randbelow(1000000):06d}'
|
||||||
tz_offset = _tz_to_offset_seconds(tz)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_send_verification_email(account['email_address'], code)
|
_send_verification_email(account['email_address'], code)
|
||||||
|
|
@ -101,15 +90,15 @@ def form_create():
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
con.execute(
|
con.execute(
|
||||||
'''INSERT INTO clients
|
'''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 (?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?)
|
||||||
ON CONFLICT(cookie_unique_token) DO UPDATE SET
|
ON CONFLICT(cookie_unique_token) DO UPDATE SET
|
||||||
email=excluded.email,
|
email=excluded.email,
|
||||||
hashed_password=excluded.hashed_password,
|
hashed_password=excluded.hashed_password,
|
||||||
tz_offset_seconds=excluded.tz_offset_seconds,
|
timezone=excluded.timezone,
|
||||||
verification_code=excluded.verification_code,
|
verification_code=excluded.verification_code,
|
||||||
code_sent_ts=excluded.code_sent_ts''',
|
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.commit()
|
||||||
con.close()
|
con.close()
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ def form_login():
|
||||||
|
|
||||||
session.clear()
|
session.clear()
|
||||||
session['account_id'] = account['account_id']
|
session['account_id'] = account['account_id']
|
||||||
session['tz_offset_seconds'] = settings.get_host_utc_offset()
|
|
||||||
session['timezone'] = settings.get_host_timezone()
|
session['timezone'] = settings.get_host_timezone()
|
||||||
session['apply_changes_immediately'] = False
|
session['apply_changes_immediately'] = False
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ def email_verify():
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
con.execute(
|
con.execute(
|
||||||
'''UPDATE clients SET email=NULL, hashed_password=NULL,
|
'''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=?''',
|
WHERE cookie_unique_token=?''',
|
||||||
(token,)
|
(token,)
|
||||||
)
|
)
|
||||||
|
|
@ -71,7 +71,7 @@ def email_verify():
|
||||||
)
|
)
|
||||||
con.execute(
|
con.execute(
|
||||||
'''UPDATE clients SET email=NULL, hashed_password=NULL,
|
'''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=?''',
|
WHERE cookie_unique_token=?''',
|
||||||
(token,)
|
(token,)
|
||||||
)
|
)
|
||||||
|
|
@ -82,7 +82,7 @@ def email_verify():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
session['account_id'] = account['account_id']
|
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['apply_changes_immediately'] = False
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,6 @@ _PAGE = Path(__file__).parent.name
|
||||||
bp = Blueprint(_PAGE, __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'])
|
@bp.route('/action/preferences/accountdetails_save', methods=['POST'])
|
||||||
@auth.require_level('viewer')
|
@auth.require_level('viewer')
|
||||||
def accountdetails_save():
|
def accountdetails_save():
|
||||||
|
|
@ -29,9 +19,7 @@ def accountdetails_save():
|
||||||
flash('Timezone is required.', 'error')
|
flash('Timezone is required.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
tz_offset = _tz_to_offset_seconds(tz)
|
|
||||||
session['timezone'] = tz
|
session['timezone'] = tz
|
||||||
session['tz_offset_seconds'] = tz_offset
|
|
||||||
|
|
||||||
flash('Preferences saved.', 'success')
|
flash('Preferences saved.', 'success')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
from flask.sessions import SessionInterface, SessionMixin
|
from flask.sessions import SessionInterface, SessionMixin
|
||||||
from werkzeug.datastructures import CallbackDict
|
from werkzeug.datastructures import CallbackDict
|
||||||
|
import settings as _settings
|
||||||
|
|
||||||
_LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'}
|
_LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'}
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ class SqliteSessionInterface(SessionInterface):
|
||||||
try:
|
try:
|
||||||
con = self._connect()
|
con = self._connect()
|
||||||
row = con.execute(
|
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,
|
s.preferences_json, s.flashes_json,
|
||||||
a.email, a.access_level
|
a.email, a.access_level
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
|
|
@ -47,12 +48,12 @@ class SqliteSessionInterface(SessionInterface):
|
||||||
if row:
|
if row:
|
||||||
prefs = json.loads(row['preferences_json'] or '{}')
|
prefs = json.loads(row['preferences_json'] or '{}')
|
||||||
flashes = json.loads(row['flashes_json'] or '[]')
|
flashes = json.loads(row['flashes_json'] or '[]')
|
||||||
|
tz = str(row['timezone'] or '') or _settings.get_host_timezone()
|
||||||
data = {
|
data = {
|
||||||
'account_id': str(row['account_id']),
|
'account_id': str(row['account_id']),
|
||||||
'email_address': str(row['email']),
|
'email_address': str(row['email']),
|
||||||
'access_level': _LEVEL_INT_TO_STR.get(row['access_level'], 'viewer'),
|
'access_level': _LEVEL_INT_TO_STR.get(row['access_level'], 'viewer'),
|
||||||
'tz_offset_seconds': int(row['tz_offset_seconds']),
|
'timezone': tz,
|
||||||
'timezone': str(prefs.get('timezone', '')),
|
|
||||||
'apply_changes_immediately': bool(prefs.get('apply_changes_immediately', False)),
|
'apply_changes_immediately': bool(prefs.get('apply_changes_immediately', False)),
|
||||||
'_flashes': flashes,
|
'_flashes': flashes,
|
||||||
'_permanent': True,
|
'_permanent': True,
|
||||||
|
|
@ -94,23 +95,22 @@ class SqliteSessionInterface(SessionInterface):
|
||||||
con = self._connect()
|
con = self._connect()
|
||||||
if account_id:
|
if account_id:
|
||||||
prefs = json.dumps({
|
prefs = json.dumps({
|
||||||
'timezone': session.get('timezone', ''),
|
|
||||||
'apply_changes_immediately': bool(session.get('apply_changes_immediately', False)),
|
'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 OR IGNORE INTO clients (cookie_unique_token) VALUES (?)', (session.sid,))
|
||||||
con.execute(
|
con.execute(
|
||||||
'''INSERT INTO sessions
|
'''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)
|
flashes_json, session_started_ts, last_seen_ts)
|
||||||
VALUES (?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?)
|
||||||
ON CONFLICT(session_id) DO UPDATE SET
|
ON CONFLICT(session_id) DO UPDATE SET
|
||||||
account_id=excluded.account_id,
|
account_id=excluded.account_id,
|
||||||
tz_offset_seconds=excluded.tz_offset_seconds,
|
timezone=excluded.timezone,
|
||||||
preferences_json=excluded.preferences_json,
|
preferences_json=excluded.preferences_json,
|
||||||
flashes_json=excluded.flashes_json,
|
flashes_json=excluded.flashes_json,
|
||||||
last_seen_ts=excluded.last_seen_ts''',
|
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:
|
else:
|
||||||
con.execute(
|
con.execute(
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ def get_host_utc_offset():
|
||||||
return time.localtime().tm_gmtoff
|
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():
|
def get_host_timezone():
|
||||||
"""Return the host timezone name (e.g. 'America/New_York'), or '' if unknown."""
|
"""Return the host timezone name (e.g. 'America/New_York'), or '' if unknown."""
|
||||||
tz = os.environ.get('TZ', '').strip()
|
tz = os.environ.get('TZ', '').strip()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue