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'])
|
@bp.route('/action/accountlogout/logout', methods=['POST'])
|
||||||
@auth.require_level('viewer')
|
@auth.require_level('viewer')
|
||||||
def logout():
|
def logout():
|
||||||
sid = session.get('session_id', '')
|
|
||||||
if sid:
|
|
||||||
import config_utils as _cu
|
|
||||||
_cu.record_session_logout(sid)
|
|
||||||
session.clear()
|
session.clear()
|
||||||
return redirect('/overview')
|
return redirect('/overview')
|
||||||
|
|
|
||||||
|
|
@ -610,72 +610,6 @@ def revert_group_chain(group_uuid):
|
||||||
return errors, succeeded, failed
|
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 ==============================================================
|
# Misc ==============================================================
|
||||||
|
|
||||||
def run_apply():
|
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 action_accountlogout import bp as accountlogout_bp
|
||||||
from api_apply_health import bp as api_apply_health_bp
|
from api_apply_health import bp as api_apply_health_bp
|
||||||
|
|
||||||
|
from session_interface import SqliteSessionInterface
|
||||||
|
|
||||||
app = Flask(__name__)
|
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 =================================================
|
# Static www/ serving =================================================
|
||||||
|
|
||||||
|
|
@ -77,9 +80,6 @@ def serve_view(page_name):
|
||||||
|
|
||||||
view_req = view_def.get('client_requirement')
|
view_req = view_def.get('client_requirement')
|
||||||
level = factory.client_level()
|
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):
|
if not factory.passes(view_req, level):
|
||||||
return redirect('/overview' if level > 0 else '/accountlogin')
|
return redirect('/overview' if level > 0 else '/accountlogin')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,4 @@ def form_login():
|
||||||
session['apply_changes_immediately'] = False
|
session['apply_changes_immediately'] = False
|
||||||
session.permanent = True
|
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')
|
return redirect('/overview')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Blueprint, request, session, redirect, flash
|
from flask import Blueprint, request, session, redirect, flash
|
||||||
import json, re
|
import json, os, re, sqlite3
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import auth
|
import auth
|
||||||
import config_utils
|
import config_utils
|
||||||
|
|
@ -100,6 +100,24 @@ def accounts_edit():
|
||||||
return redirect(f'/{_PAGE}')
|
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'])
|
@bp.route('/action/accountmanage/accounts_delete', methods=['POST'])
|
||||||
@auth.require_level('manager')
|
@auth.require_level('manager')
|
||||||
def accounts_delete():
|
def accounts_delete():
|
||||||
|
|
@ -118,7 +136,10 @@ def accounts_delete():
|
||||||
|
|
||||||
target = accounts[row_index]
|
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')
|
flash('You cannot remove your own account.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import config_utils
|
import config_utils
|
||||||
|
|
@ -14,28 +15,53 @@ def _fmt_ts(ts):
|
||||||
|
|
||||||
|
|
||||||
def _active_sessions_table():
|
def _active_sessions_table():
|
||||||
rows = config_utils.get_active_sessions()
|
try:
|
||||||
no_data = '<p class="text-muted" style="margin:0">No active sessions.</p>'
|
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:
|
if not rows:
|
||||||
return no_data
|
return '<p class="text-muted" style="margin:0">No active sessions.</p>'
|
||||||
|
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
trs = ''
|
trs = ''
|
||||||
for email, access_level, logged_in_at, last_seen in rows:
|
for sid, email, access_level, created_at, last_seen in rows:
|
||||||
ago = config_utils.relative_time(int(last_seen), now)
|
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 += (
|
trs += (
|
||||||
f'<tr>'
|
f'<tr>'
|
||||||
f'<td class="table-cell">{factory.e(email)}</td>'
|
f'<td class="table-cell">{factory.e(email)}</td>'
|
||||||
f'<td class="table-cell">{factory.e(access_level)}</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">{badge}</td>'
|
||||||
f'<td class="table-cell">{factory.e(ago)} ago</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>'
|
f'</tr>'
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
'<table class="data-table"><thead><tr>'
|
'<table class="data-table"><thead><tr>'
|
||||||
'<th class="table-header">Email</th>'
|
'<th class="table-header">Email</th>'
|
||||||
'<th class="table-header">Access Level</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">Logged In</th>'
|
||||||
'<th class="table-header">Last Seen</th>'
|
'<th class="table-header">Last Seen</th>'
|
||||||
|
'<th class="table-header"></th>'
|
||||||
'</tr></thead><tbody>' + trs + '</tbody></table>'
|
'</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),
|
||||||
|
)
|
||||||
|
|
@ -25,6 +25,5 @@ services:
|
||||||
- SMTP_USER=grotek.industries@gmail.com
|
- SMTP_USER=grotek.industries@gmail.com
|
||||||
- SMTP_PASSWORD=lfhrygyuwvlaczaw
|
- SMTP_PASSWORD=lfhrygyuwvlaczaw
|
||||||
- SMTP_FROM=grotek.industries@gmail.com
|
- SMTP_FROM=grotek.industries@gmail.com
|
||||||
- LICENSE=asdf
|
|
||||||
- DEV_MODE=true
|
- DEV_MODE=true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue