Development
This commit is contained in:
parent
fff0835553
commit
19be151c70
7 changed files with 230 additions and 31 deletions
|
|
@ -7,5 +7,9 @@ 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')
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ from flask import session
|
||||||
APP_DIR = _os.path.dirname(_os.path.abspath(__file__))
|
APP_DIR = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
CONFIGS_DIR = '/routlin_location'
|
CONFIGS_DIR = '/routlin_location'
|
||||||
WWW_DIR = '/www'
|
WWW_DIR = '/www'
|
||||||
ACCOUNTS_FILE = f'{APP_DIR}/authorized_accounts.json'
|
ACCOUNTS_FILE = f'{CONFIGS_DIR}/.dashboard-accounts'
|
||||||
|
SESSIONS_DB = f'{CONFIGS_DIR}/.dashboard-sessions'
|
||||||
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
||||||
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
||||||
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
||||||
|
|
@ -609,6 +610,72 @@ 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():
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,9 @@ 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,4 +56,10 @@ 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')
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,40 @@ def accounts_add():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/accountmanage/accounts_edit', methods=['POST'])
|
||||||
|
@auth.require_level('manager')
|
||||||
|
def accounts_edit():
|
||||||
|
try:
|
||||||
|
row_index = int(request.form.get('row_index', ''))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
flash('Invalid request.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
access_level = request.form.get('access_level', '').strip()
|
||||||
|
if access_level not in VALID_LEVELS:
|
||||||
|
flash('Invalid access level.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
data = _load_accounts()
|
||||||
|
accounts = data.get('accounts', [])
|
||||||
|
|
||||||
|
if row_index < 0 or row_index >= len(accounts):
|
||||||
|
flash('Account not found.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
target = accounts[row_index]
|
||||||
|
if target.get('email_address', '').lower() == session.get('email_address', '').lower():
|
||||||
|
flash('You cannot change your own access level.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
accounts[row_index]['access_level'] = access_level
|
||||||
|
data['accounts'] = accounts
|
||||||
|
_save_accounts(data)
|
||||||
|
|
||||||
|
flash('Account updated.', 'success')
|
||||||
|
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():
|
||||||
|
|
|
||||||
|
|
@ -10,27 +10,29 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Active Sessions",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "%ACTIVE_SESSIONS_TABLE%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "User Accounts",
|
||||||
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"datasource": "config:accounts",
|
"datasource": "config:accounts",
|
||||||
"empty_message": "No accounts configured.",
|
"empty_message": "No accounts configured.",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{"label": "Email Address", "field": "email_address"},
|
||||||
"label": "Email Address",
|
{"label": "Access Level", "field": "access_level"},
|
||||||
"field": "email_address"
|
{"label": "Added By", "field": "account_created_by"},
|
||||||
},
|
{"label": "Added", "field": "account_created_utc"},
|
||||||
{
|
|
||||||
"label": "Access Level",
|
|
||||||
"field": "access_level"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Added By",
|
|
||||||
"field": "account_created_by"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Added",
|
|
||||||
"field": "account_created_utc"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"field": "account_status",
|
"field": "account_status",
|
||||||
|
|
@ -38,13 +40,21 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_actions": [
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"method": "js_edit",
|
||||||
|
"target": "edit-form",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"action": "/action/accountmanage/accounts_delete",
|
"action": "/action/accountmanage/accounts_delete",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"text": "Remove",
|
"text": "Delete",
|
||||||
"class": "btn-danger btn-sm"
|
"class": "btn-danger btn-sm"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "card",
|
"type": "card",
|
||||||
|
|
@ -84,6 +94,43 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "edit-form",
|
||||||
|
"label": "Edit Account",
|
||||||
|
"hidden": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/accountmanage/accounts_edit",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{"type": "hidden", "name": "row_index", "value": ""},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Email Address",
|
||||||
|
"name": "email_address_display",
|
||||||
|
"input_type": "text",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Access Level",
|
||||||
|
"name": "access_level",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%ACCOUNT_LEVEL_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{"type": "button_primary", "text": "Save Changes"},
|
||||||
|
{"type": "button_cancel", "text": "Cancel"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,45 @@
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
import config_utils
|
import config_utils
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_ts(ts):
|
||||||
|
try:
|
||||||
|
dt = datetime.fromtimestamp(int(ts), tz=timezone.utc)
|
||||||
|
return dt.strftime('%Y-%m-%d %H:%M UTC')
|
||||||
|
except Exception:
|
||||||
|
return '-'
|
||||||
|
|
||||||
|
|
||||||
|
def _active_sessions_table():
|
||||||
|
rows = config_utils.get_active_sessions()
|
||||||
|
no_data = '<p class="text-muted" style="margin:0">No active sessions.</p>'
|
||||||
|
if not rows:
|
||||||
|
return no_data
|
||||||
|
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)
|
||||||
|
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'</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">Logged In</th>'
|
||||||
|
'<th class="table-header">Last Seen</th>'
|
||||||
|
'</tr></thead><tbody>' + trs + '</tbody></table>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def collect_tokens(cfg):
|
def collect_tokens(cfg):
|
||||||
tokens = config_utils.collect_layout_tokens(cfg)
|
tokens = config_utils.collect_layout_tokens(cfg)
|
||||||
tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([
|
tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([
|
||||||
|
|
@ -10,6 +47,7 @@ def collect_tokens(cfg):
|
||||||
{'value': 'administrator', 'label': 'Administrator (can modify configuration)'},
|
{'value': 'administrator', 'label': 'Administrator (can modify configuration)'},
|
||||||
{'value': 'manager', 'label': 'Manager (full access including account management)'},
|
{'value': 'manager', 'label': 'Manager (full access including account management)'},
|
||||||
])
|
])
|
||||||
|
tokens['ACTIVE_SESSIONS_TABLE'] = _active_sessions_table()
|
||||||
content = factory.load_json(f'{factory.PAGES_DIR}/accountmanage/content.json')
|
content = factory.load_json(f'{factory.PAGES_DIR}/accountmanage/content.json')
|
||||||
for table_item in factory.iter_table_items(content.get('items', [])):
|
for table_item in factory.iter_table_items(content.get('items', [])):
|
||||||
ds = table_item.get('datasource', '')
|
ds = table_item.get('datasource', '')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue