Development
This commit is contained in:
parent
edeb05acf7
commit
a886a56982
4 changed files with 142 additions and 108 deletions
|
|
@ -42,26 +42,31 @@ def init_accounts_db():
|
||||||
con.executescript('''
|
con.executescript('''
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
account_id TEXT PRIMARY KEY,
|
account_id TEXT PRIMARY KEY,
|
||||||
email TEXT UNIQUE NOT NULL,
|
email TEXT NOT NULL UNIQUE,
|
||||||
access_level INTEGER NOT NULL DEFAULT 1,
|
access_level INTEGER NOT NULL,
|
||||||
hashed_password TEXT NOT NULL DEFAULT '',
|
hashed_password TEXT,
|
||||||
created_ts INTEGER NOT NULL DEFAULT 0,
|
created_ts INTEGER NOT NULL,
|
||||||
created_by TEXT NOT NULL DEFAULT ''
|
created_by TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS clients (
|
||||||
|
cookie_unique_token TEXT PRIMARY KEY,
|
||||||
|
email TEXT UNIQUE,
|
||||||
|
hashed_password TEXT,
|
||||||
|
tz_offset_seconds INTEGER,
|
||||||
|
verification_code TEXT,
|
||||||
|
code_sent_ts INTEGER,
|
||||||
|
flashes_json TEXT
|
||||||
);
|
);
|
||||||
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 DEFAULT 0,
|
tz_offset_seconds INTEGER NOT NULL,
|
||||||
apply_changes_immediately INTEGER NOT NULL DEFAULT 0,
|
preferences_json TEXT NOT NULL,
|
||||||
|
flashes_json TEXT,
|
||||||
session_started_ts INTEGER NOT NULL,
|
session_started_ts INTEGER NOT NULL,
|
||||||
last_seen_ts INTEGER NOT NULL
|
last_seen_ts INTEGER NOT NULL,
|
||||||
);
|
FOREIGN KEY (session_id) REFERENCES clients(cookie_unique_token),
|
||||||
CREATE TABLE IF NOT EXISTS pending_verifications (
|
FOREIGN KEY (account_id) REFERENCES accounts(account_id)
|
||||||
email TEXT PRIMARY KEY,
|
|
||||||
hashed_password TEXT NOT NULL,
|
|
||||||
tz_offset_seconds INTEGER NOT NULL DEFAULT 0,
|
|
||||||
code TEXT NOT NULL,
|
|
||||||
expires_ts INTEGER NOT NULL
|
|
||||||
);
|
);
|
||||||
''')
|
''')
|
||||||
con.commit()
|
con.commit()
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,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}'
|
||||||
expires_ts = int(time.time()) + CODE_TTL_SECS
|
|
||||||
tz_offset = _tz_to_offset_seconds(tz)
|
tz_offset = _tz_to_offset_seconds(tz)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -101,10 +100,16 @@ def form_create():
|
||||||
try:
|
try:
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
con.execute(
|
con.execute(
|
||||||
'''INSERT OR REPLACE INTO pending_verifications
|
'''INSERT INTO clients
|
||||||
(email, hashed_password, tz_offset_seconds, code, expires_ts)
|
(cookie_unique_token, email, hashed_password, tz_offset_seconds, verification_code, code_sent_ts)
|
||||||
VALUES (?,?,?,?,?)''',
|
VALUES (?,?,?,?,?,?)
|
||||||
(account['email_address'].lower(), hashed, tz_offset, code, expires_ts)
|
ON CONFLICT(cookie_unique_token) DO UPDATE SET
|
||||||
|
email=excluded.email,
|
||||||
|
hashed_password=excluded.hashed_password,
|
||||||
|
tz_offset_seconds=excluded.tz_offset_seconds,
|
||||||
|
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()))
|
||||||
)
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
|
|
@ -112,6 +117,4 @@ def form_create():
|
||||||
flash(f'Could not store verification: {exc}', 'error')
|
flash(f'Could not store verification: {exc}', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
session['pending_verify_email'] = account['email_address']
|
|
||||||
|
|
||||||
return redirect('/accountverifyemail')
|
return redirect('/accountverifyemail')
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ from flask import Blueprint, request, session, redirect, flash
|
||||||
import time, secrets
|
import time, secrets
|
||||||
import auth
|
import auth
|
||||||
import config_utils
|
import config_utils
|
||||||
import settings
|
|
||||||
|
|
||||||
_PAGE = Path(__file__).parent.name
|
_PAGE = Path(__file__).parent.name
|
||||||
|
|
||||||
|
|
@ -16,49 +15,50 @@ def email_verify():
|
||||||
if session.get('access_level', 'nothing') != 'nothing':
|
if session.get('access_level', 'nothing') != 'nothing':
|
||||||
return redirect('/overview')
|
return redirect('/overview')
|
||||||
|
|
||||||
pending_email = session.get('pending_verify_email', '').lower()
|
from pages.accountcreate.action import CODE_TTL_SECS
|
||||||
if not pending_email:
|
|
||||||
flash('No pending account creation found. Please start over.', 'error')
|
|
||||||
return redirect('/accountcreate')
|
|
||||||
|
|
||||||
|
token = session.sid
|
||||||
try:
|
try:
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
row = con.execute(
|
client = con.execute(
|
||||||
'SELECT * FROM pending_verifications WHERE email=?', (pending_email,)
|
'SELECT * FROM clients WHERE cookie_unique_token=?', (token,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
con.close()
|
con.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
row = None
|
client = None
|
||||||
|
|
||||||
if not row:
|
if not client or not client['email']:
|
||||||
flash('No pending account creation found. Please start over.', 'error')
|
flash('No pending account creation found. Please start over.', 'error')
|
||||||
return redirect('/accountcreate')
|
return redirect('/accountcreate')
|
||||||
|
|
||||||
if int(time.time()) > row['expires_ts']:
|
if int(time.time()) > client['code_sent_ts'] + CODE_TTL_SECS:
|
||||||
try:
|
try:
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
con.execute('DELETE FROM pending_verifications WHERE email=?', (pending_email,))
|
con.execute(
|
||||||
|
'''UPDATE clients SET email=NULL, hashed_password=NULL,
|
||||||
|
tz_offset_seconds=NULL, verification_code=NULL, code_sent_ts=NULL
|
||||||
|
WHERE cookie_unique_token=?''',
|
||||||
|
(token,)
|
||||||
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
session.pop('pending_verify_email', None)
|
|
||||||
flash('Verification code has expired. Please start over.', 'error')
|
flash('Verification code has expired. Please start over.', 'error')
|
||||||
return redirect('/accountcreate')
|
return redirect('/accountcreate')
|
||||||
|
|
||||||
submitted = request.form.get('code', '').strip()
|
submitted = request.form.get('code', '').strip()
|
||||||
if submitted != row['code']:
|
if submitted != client['verification_code']:
|
||||||
flash('Incorrect verification code.', 'error')
|
flash('Incorrect verification code.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
pending_email = client['email']
|
||||||
account = config_utils.get_account_by_email(pending_email)
|
account = config_utils.get_account_by_email(pending_email)
|
||||||
if account is None:
|
if account is None:
|
||||||
session.pop('pending_verify_email', None)
|
|
||||||
flash('Account no longer exists. Contact your manager.', 'error')
|
flash('Account no longer exists. Contact your manager.', 'error')
|
||||||
return redirect('/accountcreate')
|
return redirect('/accountcreate')
|
||||||
|
|
||||||
if account.get('hashed_password'):
|
if account.get('hashed_password'):
|
||||||
session.pop('pending_verify_email', None)
|
|
||||||
flash('This account is already set up. Please log in.', 'error')
|
flash('This account is already set up. Please log in.', 'error')
|
||||||
return redirect('/accountlogin')
|
return redirect('/accountlogin')
|
||||||
|
|
||||||
|
|
@ -67,18 +67,22 @@ def email_verify():
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
con.execute(
|
con.execute(
|
||||||
'UPDATE accounts SET hashed_password=?, created_ts=?, created_by=? WHERE account_id=?',
|
'UPDATE accounts SET hashed_password=?, created_ts=?, created_by=? WHERE account_id=?',
|
||||||
(row['hashed_password'], now, 'self', account['account_id'])
|
(client['hashed_password'], now, 'self', account['account_id'])
|
||||||
|
)
|
||||||
|
con.execute(
|
||||||
|
'''UPDATE clients SET email=NULL, hashed_password=NULL,
|
||||||
|
tz_offset_seconds=NULL, verification_code=NULL, code_sent_ts=NULL
|
||||||
|
WHERE cookie_unique_token=?''',
|
||||||
|
(token,)
|
||||||
)
|
)
|
||||||
con.execute('DELETE FROM pending_verifications WHERE email=?', (pending_email,))
|
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
flash(f'Could not complete account setup: {exc}', 'error')
|
flash(f'Could not complete account setup: {exc}', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
session.pop('pending_verify_email', None)
|
|
||||||
session['account_id'] = account['account_id']
|
session['account_id'] = account['account_id']
|
||||||
session['tz_offset_seconds'] = int(row['tz_offset_seconds'])
|
session['tz_offset_seconds'] = int(client['tz_offset_seconds'])
|
||||||
session['apply_changes_immediately'] = False
|
session['apply_changes_immediately'] = False
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
|
|
||||||
|
|
@ -91,18 +95,27 @@ def email_resend():
|
||||||
if session.get('access_level', 'nothing') != 'nothing':
|
if session.get('access_level', 'nothing') != 'nothing':
|
||||||
return redirect('/overview')
|
return redirect('/overview')
|
||||||
|
|
||||||
from pages.accountcreate.action import _send_verification_email, CODE_TTL_SECS
|
from pages.accountcreate.action import _send_verification_email
|
||||||
|
|
||||||
pending_email = session.get('pending_verify_email', '').lower()
|
token = session.sid
|
||||||
if not pending_email:
|
try:
|
||||||
|
con = config_utils.open_accounts_db()
|
||||||
|
client = con.execute(
|
||||||
|
'SELECT * FROM clients WHERE cookie_unique_token=?', (token,)
|
||||||
|
).fetchone()
|
||||||
|
con.close()
|
||||||
|
except Exception:
|
||||||
|
client = None
|
||||||
|
|
||||||
|
if not client or not client['email']:
|
||||||
flash('No pending account creation found. Please start over.', 'error')
|
flash('No pending account creation found. Please start over.', 'error')
|
||||||
return redirect('/accountcreate')
|
return redirect('/accountcreate')
|
||||||
|
|
||||||
code = f'{secrets.randbelow(1000000):06d}'
|
code = f'{secrets.randbelow(1000000):06d}'
|
||||||
expires_ts = int(time.time()) + CODE_TTL_SECS
|
code_sent_ts = int(time.time())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_send_verification_email(pending_email, code)
|
_send_verification_email(client['email'], code)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
flash(f'Could not resend verification email: {exc}', 'error')
|
flash(f'Could not resend verification email: {exc}', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
@ -110,8 +123,8 @@ def email_resend():
|
||||||
try:
|
try:
|
||||||
con = config_utils.open_accounts_db()
|
con = config_utils.open_accounts_db()
|
||||||
con.execute(
|
con.execute(
|
||||||
'UPDATE pending_verifications SET code=?, expires_ts=? WHERE email=?',
|
'UPDATE clients SET verification_code=?, code_sent_ts=? WHERE cookie_unique_token=?',
|
||||||
(code, expires_ts, pending_email)
|
(code, code_sent_ts, token)
|
||||||
)
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -30,39 +31,49 @@ class SqliteSessionInterface(SessionInterface):
|
||||||
def open_session(self, app, request):
|
def open_session(self, app, request):
|
||||||
name = app.config.get('SESSION_COOKIE_NAME', 'session')
|
name = app.config.get('SESSION_COOKIE_NAME', 'session')
|
||||||
sid = request.cookies.get(name)
|
sid = request.cookies.get(name)
|
||||||
if sid:
|
if not sid:
|
||||||
|
return SqliteSession(sid=str(uuid.uuid4()), new=True)
|
||||||
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.tz_offset_seconds,
|
||||||
s.apply_changes_immediately,
|
s.preferences_json, s.flashes_json,
|
||||||
a.email, a.access_level
|
a.email, a.access_level
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN accounts a ON a.account_id = s.account_id
|
JOIN accounts a ON a.account_id = s.account_id
|
||||||
WHERE s.session_id=?''',
|
WHERE s.session_id=?''',
|
||||||
(sid,)
|
(sid,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
con.close()
|
|
||||||
if row:
|
if row:
|
||||||
|
prefs = json.loads(row['preferences_json'] or '{}')
|
||||||
|
flashes = json.loads(row['flashes_json'] or '[]')
|
||||||
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']),
|
'tz_offset_seconds': int(row['tz_offset_seconds']),
|
||||||
'apply_changes_immediately': bool(row['apply_changes_immediately']),
|
'apply_changes_immediately': bool(prefs.get('apply_changes_immediately', False)),
|
||||||
|
'_flashes': flashes,
|
||||||
}
|
}
|
||||||
|
con.close()
|
||||||
|
return SqliteSession(data, sid=sid, new=False)
|
||||||
|
client = con.execute(
|
||||||
|
'SELECT flashes_json FROM clients WHERE cookie_unique_token=?', (sid,)
|
||||||
|
).fetchone()
|
||||||
|
con.close()
|
||||||
|
flashes = json.loads(client['flashes_json'] or '[]') if client else []
|
||||||
|
data = {'_flashes': flashes} if flashes else {}
|
||||||
return SqliteSession(data, sid=sid, new=False)
|
return SqliteSession(data, sid=sid, new=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return SqliteSession(sid=str(uuid.uuid4()), new=True)
|
return SqliteSession(sid=sid, new=False)
|
||||||
|
|
||||||
def save_session(self, app, session, response):
|
def save_session(self, app, session, response):
|
||||||
name = app.config.get('SESSION_COOKIE_NAME', 'session')
|
name = app.config.get('SESSION_COOKIE_NAME', 'session')
|
||||||
domain = self.get_cookie_domain(app)
|
domain = self.get_cookie_domain(app)
|
||||||
path = self.get_cookie_path(app)
|
path = self.get_cookie_path(app)
|
||||||
|
|
||||||
if not session:
|
if not session and session.modified and not session.new:
|
||||||
if not session.new:
|
|
||||||
try:
|
try:
|
||||||
con = self._connect()
|
con = self._connect()
|
||||||
con.execute('DELETE FROM sessions WHERE session_id=?', (session.sid,))
|
con.execute('DELETE FROM sessions WHERE session_id=?', (session.sid,))
|
||||||
|
|
@ -74,33 +85,35 @@ class SqliteSessionInterface(SessionInterface):
|
||||||
return
|
return
|
||||||
|
|
||||||
account_id = session.get('account_id')
|
account_id = session.get('account_id')
|
||||||
if not account_id:
|
flashes_json = json.dumps(session.get('_flashes', []))
|
||||||
return
|
|
||||||
|
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
tz_offset = int(session.get('tz_offset_seconds', 0))
|
|
||||||
apply_changes = 1 if session.get('apply_changes_immediately') else 0
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
con = self._connect()
|
con = self._connect()
|
||||||
if session.new:
|
if account_id:
|
||||||
|
prefs = json.dumps({'apply_changes_immediately': bool(session.get('apply_changes_immediately', False))})
|
||||||
|
tz_offset = int(session.get('tz_offset_seconds', 0))
|
||||||
|
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, apply_changes_immediately,
|
(session_id, account_id, tz_offset_seconds, preferences_json,
|
||||||
session_started_ts, last_seen_ts)
|
flashes_json, session_started_ts, last_seen_ts)
|
||||||
VALUES (?,?,?,?,?,?)''',
|
VALUES (?,?,?,?,?,?,?)
|
||||||
(session.sid, account_id, tz_offset, apply_changes, now, now)
|
ON CONFLICT(session_id) DO UPDATE SET
|
||||||
)
|
account_id=excluded.account_id,
|
||||||
elif session.modified:
|
tz_offset_seconds=excluded.tz_offset_seconds,
|
||||||
con.execute(
|
preferences_json=excluded.preferences_json,
|
||||||
'''UPDATE sessions SET tz_offset_seconds=?, apply_changes_immediately=?,
|
flashes_json=excluded.flashes_json,
|
||||||
last_seen_ts=? WHERE session_id=?''',
|
last_seen_ts=excluded.last_seen_ts''',
|
||||||
(tz_offset, apply_changes, now, session.sid)
|
(session.sid, account_id, tz_offset, prefs, flashes_json, now, now)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
con.execute(
|
con.execute(
|
||||||
'UPDATE sessions SET last_seen_ts=? WHERE session_id=?',
|
'''INSERT INTO clients (cookie_unique_token, flashes_json)
|
||||||
(now, session.sid)
|
VALUES (?,?)
|
||||||
|
ON CONFLICT(cookie_unique_token) DO UPDATE SET
|
||||||
|
flashes_json=excluded.flashes_json''',
|
||||||
|
(session.sid, flashes_json)
|
||||||
)
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue