Development
This commit is contained in:
parent
19be151c70
commit
f5722f3c7b
8 changed files with 178 additions and 90 deletions
|
|
@ -7,9 +7,5 @@ bp = Blueprint('accountlogout', __name__)
|
|||
@bp.route('/action/accountlogout/logout', methods=['POST'])
|
||||
@auth.require_level('viewer')
|
||||
def logout():
|
||||
sid = session.get('session_id', '')
|
||||
if sid:
|
||||
import config_utils as _cu
|
||||
_cu.record_session_logout(sid)
|
||||
session.clear()
|
||||
return redirect('/overview')
|
||||
|
|
|
|||
|
|
@ -610,72 +610,6 @@ def revert_group_chain(group_uuid):
|
|||
return errors, succeeded, failed
|
||||
|
||||
|
||||
# Sessions DB =======================================================
|
||||
|
||||
def _open_sessions_db():
|
||||
import sqlite3 as _sq, time as _t
|
||||
con = _sq.connect(SESSIONS_DB, timeout=5)
|
||||
con.execute('PRAGMA journal_mode=WAL')
|
||||
con.executescript('''
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
access_level TEXT NOT NULL DEFAULT '',
|
||||
logged_in_at INTEGER NOT NULL,
|
||||
last_seen INTEGER NOT NULL
|
||||
);
|
||||
''')
|
||||
con.commit()
|
||||
return con
|
||||
|
||||
def record_session_login(session_id, email, access_level):
|
||||
import time as _t
|
||||
try:
|
||||
con = _open_sessions_db()
|
||||
now = int(_t.time())
|
||||
con.execute(
|
||||
'INSERT OR REPLACE INTO sessions(session_id, email, access_level, logged_in_at, last_seen) VALUES(?,?,?,?,?)',
|
||||
(session_id, email, access_level, now, now)
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def record_session_logout(session_id):
|
||||
try:
|
||||
con = _open_sessions_db()
|
||||
con.execute('DELETE FROM sessions WHERE session_id=?', (session_id,))
|
||||
con.commit()
|
||||
con.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def record_session_activity(session_id):
|
||||
import time as _t
|
||||
try:
|
||||
con = _open_sessions_db()
|
||||
con.execute('UPDATE sessions SET last_seen=? WHERE session_id=?', (int(_t.time()), session_id))
|
||||
con.commit()
|
||||
con.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_active_sessions():
|
||||
import time as _t
|
||||
cutoff = int(_t.time()) - 31 * 86400
|
||||
try:
|
||||
con = _open_sessions_db()
|
||||
rows = con.execute(
|
||||
'SELECT email, access_level, logged_in_at, last_seen FROM sessions WHERE last_seen > ? ORDER BY last_seen DESC',
|
||||
(cutoff,)
|
||||
).fetchall()
|
||||
con.close()
|
||||
return rows
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# Misc ==============================================================
|
||||
|
||||
def run_apply():
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ from pages.captiveportal.action import bp as captiveportal_bp
|
|||
from action_accountlogout import bp as accountlogout_bp
|
||||
from api_apply_health import bp as api_apply_health_bp
|
||||
|
||||
from session_interface import SqliteSessionInterface
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
|
||||
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
|
||||
app.session_interface = SqliteSessionInterface(config_utils.SESSIONS_DB)
|
||||
|
||||
# Static www/ serving =================================================
|
||||
|
||||
|
|
@ -77,9 +80,6 @@ def serve_view(page_name):
|
|||
|
||||
view_req = view_def.get('client_requirement')
|
||||
level = factory.client_level()
|
||||
sid = session.get('session_id', '')
|
||||
if sid and level > 0:
|
||||
config_utils.record_session_activity(sid)
|
||||
if not factory.passes(view_req, level):
|
||||
return redirect('/overview' if level > 0 else '/accountlogin')
|
||||
|
||||
|
|
|
|||
|
|
@ -56,10 +56,4 @@ def form_login():
|
|||
session['apply_changes_immediately'] = False
|
||||
session.permanent = True
|
||||
|
||||
import uuid as _uuid
|
||||
sid = str(_uuid.uuid4())
|
||||
session['session_id'] = sid
|
||||
import config_utils as _cu
|
||||
_cu.record_session_login(sid, account['email_address'], account.get('access_level', 'viewer'))
|
||||
|
||||
return redirect('/overview')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from pathlib import Path
|
||||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json, re
|
||||
import json, os, re, sqlite3
|
||||
from datetime import datetime, timezone
|
||||
import auth
|
||||
import config_utils
|
||||
|
|
@ -100,6 +100,24 @@ def accounts_edit():
|
|||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/accountmanage/session_invalidate', methods=['POST'])
|
||||
@auth.require_level('manager')
|
||||
def session_invalidate():
|
||||
sid = request.form.get('session_id', '').strip()
|
||||
if not sid:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
try:
|
||||
con = sqlite3.connect(config_utils.SESSIONS_DB, timeout=5)
|
||||
con.execute('DELETE FROM sessions WHERE session_id=?', (sid,))
|
||||
con.commit()
|
||||
con.close()
|
||||
flash('Session invalidated.', 'success')
|
||||
except Exception:
|
||||
flash('Failed to invalidate session.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/accountmanage/accounts_delete', methods=['POST'])
|
||||
@auth.require_level('manager')
|
||||
def accounts_delete():
|
||||
|
|
@ -118,7 +136,10 @@ def accounts_delete():
|
|||
|
||||
target = accounts[row_index]
|
||||
|
||||
if target.get('email_address', '').lower() == session.get('email_address', '').lower():
|
||||
target_email = target.get('email_address', '').lower()
|
||||
current_email = session.get('email_address', '').lower()
|
||||
initial_email = os.environ.get('INITIAL_MANAGER_EMAIL', '').strip().lower()
|
||||
if target_email == current_email and target_email != initial_email:
|
||||
flash('You cannot remove your own account.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
import config_utils
|
||||
|
|
@ -14,28 +15,53 @@ def _fmt_ts(ts):
|
|||
|
||||
|
||||
def _active_sessions_table():
|
||||
rows = config_utils.get_active_sessions()
|
||||
no_data = '<p class="text-muted" style="margin:0">No active sessions.</p>'
|
||||
try:
|
||||
con = sqlite3.connect(config_utils.SESSIONS_DB, timeout=5)
|
||||
rows = con.execute(
|
||||
'SELECT session_id, email, access_level, created_at, last_seen'
|
||||
' FROM sessions ORDER BY last_seen DESC'
|
||||
).fetchall()
|
||||
con.close()
|
||||
except Exception:
|
||||
rows = []
|
||||
|
||||
if not rows:
|
||||
return no_data
|
||||
return '<p class="text-muted" style="margin:0">No active sessions.</p>'
|
||||
|
||||
now = int(time.time())
|
||||
trs = ''
|
||||
for email, access_level, logged_in_at, last_seen in rows:
|
||||
ago = config_utils.relative_time(int(last_seen), now)
|
||||
for sid, email, access_level, created_at, last_seen in rows:
|
||||
online = (now - int(last_seen)) < 300
|
||||
badge = (
|
||||
'<span class="badge badge-enabled">Online</span>'
|
||||
if online else
|
||||
'<span class="badge badge-disabled">Offline</span>'
|
||||
)
|
||||
btn = (
|
||||
f'<form method="post" action="/action/accountmanage/session_invalidate"'
|
||||
f' style="display:inline;margin:0">'
|
||||
f'<input type="hidden" name="session_id" value="{factory.e(sid)}">'
|
||||
f'<button type="submit" class="btn btn-danger btn-sm">Invalidate</button>'
|
||||
f'</form>'
|
||||
)
|
||||
trs += (
|
||||
f'<tr>'
|
||||
f'<td class="table-cell">{factory.e(email)}</td>'
|
||||
f'<td class="table-cell">{factory.e(access_level)}</td>'
|
||||
f'<td class="table-cell">{_fmt_ts(logged_in_at)}</td>'
|
||||
f'<td class="table-cell">{factory.e(ago)} ago</td>'
|
||||
f'<td class="table-cell">{badge}</td>'
|
||||
f'<td class="table-cell">{_fmt_ts(created_at)}</td>'
|
||||
f'<td class="table-cell">{_fmt_ts(last_seen)}</td>'
|
||||
f'<td class="table-cell">{btn}</td>'
|
||||
f'</tr>'
|
||||
)
|
||||
return (
|
||||
'<table class="data-table"><thead><tr>'
|
||||
'<th class="table-header">Email</th>'
|
||||
'<th class="table-header">Access Level</th>'
|
||||
'<th class="table-header">Status</th>'
|
||||
'<th class="table-header">Logged In</th>'
|
||||
'<th class="table-header">Last Seen</th>'
|
||||
'<th class="table-header"></th>'
|
||||
'</tr></thead><tbody>' + trs + '</tbody></table>'
|
||||
)
|
||||
|
||||
|
|
|
|||
118
docker/routlin-dash/app/session_interface.py
Normal file
118
docker/routlin-dash/app/session_interface.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
import uuid
|
||||
from flask.sessions import SessionInterface, SessionMixin
|
||||
from werkzeug.datastructures import CallbackDict
|
||||
|
||||
|
||||
class SqliteSession(CallbackDict, SessionMixin):
|
||||
def __init__(self, initial=None, sid=None, new=False):
|
||||
def on_update(self):
|
||||
self.modified = True
|
||||
CallbackDict.__init__(self, initial or {}, on_update)
|
||||
self.sid = sid
|
||||
self.new = new
|
||||
self.modified = False
|
||||
|
||||
|
||||
class SqliteSessionInterface(SessionInterface):
|
||||
def __init__(self, db_path):
|
||||
self.db_path = db_path
|
||||
self._init_db()
|
||||
|
||||
def _connect(self):
|
||||
con = sqlite3.connect(self.db_path, timeout=5)
|
||||
con.execute('PRAGMA journal_mode=WAL')
|
||||
return con
|
||||
|
||||
def _init_db(self):
|
||||
try:
|
||||
con = self._connect()
|
||||
con.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
access_level TEXT NOT NULL DEFAULT '',
|
||||
data_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at INTEGER NOT NULL,
|
||||
last_seen INTEGER NOT NULL
|
||||
)
|
||||
''')
|
||||
con.commit()
|
||||
con.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def open_session(self, app, request):
|
||||
name = app.config.get('SESSION_COOKIE_NAME', 'session')
|
||||
sid = request.cookies.get(name)
|
||||
if sid:
|
||||
try:
|
||||
con = self._connect()
|
||||
row = con.execute(
|
||||
'SELECT data_json FROM sessions WHERE session_id=?', (sid,)
|
||||
).fetchone()
|
||||
con.close()
|
||||
if row:
|
||||
return SqliteSession(json.loads(row[0]), sid=sid, new=False)
|
||||
except Exception:
|
||||
pass
|
||||
return SqliteSession(sid=str(uuid.uuid4()), new=True)
|
||||
|
||||
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:
|
||||
if 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
|
||||
|
||||
now = int(time.time())
|
||||
email = session.get('email_address', '')
|
||||
level = session.get('access_level', '')
|
||||
|
||||
try:
|
||||
con = self._connect()
|
||||
if session.new:
|
||||
if not session.modified:
|
||||
con.close()
|
||||
return
|
||||
con.execute(
|
||||
'INSERT INTO sessions(session_id,email,access_level,data_json,created_at,last_seen)'
|
||||
' VALUES(?,?,?,?,?,?)',
|
||||
(session.sid, email, level, json.dumps(dict(session)), now, now)
|
||||
)
|
||||
elif session.modified:
|
||||
con.execute(
|
||||
'UPDATE sessions SET email=?,access_level=?,data_json=?,last_seen=? WHERE session_id=?',
|
||||
(email, level, json.dumps(dict(session)), now, session.sid)
|
||||
)
|
||||
else:
|
||||
con.execute(
|
||||
'UPDATE sessions SET last_seen=? WHERE session_id=?',
|
||||
(now, session.sid)
|
||||
)
|
||||
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),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue