Add active user counter logic; add to admin tools.

Ports in lightly modified logic from the upstream which tracks active
sessions to provide counters and listings to understand site traffic
in the admin panel.
This commit is contained in:
TLSM 2022-09-05 00:41:17 -04:00 committed by Ben Rog-Wilhelm
parent ecbd8179b9
commit 129d644a3f
8 changed files with 125 additions and 31 deletions

View file

@ -88,25 +88,29 @@ mail = Mail(app)
@app.before_request @app.before_request
def before_request(): def before_request():
with open('site_settings.json', 'r') as f: with open('site_settings.json', 'r') as f:
app.config['SETTINGS'] = json.load(f) app.config['SETTINGS'] = json.load(f)
if request.host != app.config["SERVER_NAME"]: return {"error":"Unauthorized host provided."}, 401 if request.host != app.config["SERVER_NAME"]:
if request.headers.get("CF-Worker"): return {"error":"Cloudflare workers are not allowed to access this website."}, 401 return {"error": "Unauthorized host provided."}, 401
if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): abort(503) if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"):
abort(503)
g.db = db_session() g.db = db_session()
ua = request.headers.get("User-Agent","").lower() g.agent = request.headers.get("User-Agent")
if not g.agent:
if '; wv) ' in ua: g.webview = True return 'Please use a "User-Agent" header!', 403
else: g.webview = False
if 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua: g.inferior_browser = True
else: g.inferior_browser = False
ua = g.agent.lower()
g.webview = ('; wv) ' in ua)
g.inferior_browser = (
'iphone' in ua or
'ipad' in ua or
'ipod' in ua or
'mac os' in ua or
' firefox/' in ua)
g.timestamp = int(time.time()) g.timestamp = int(time.time())

View file

@ -72,6 +72,8 @@ PUSHER_KEY = environ.get("PUSHER_KEY", "").strip()
DEFAULT_COLOR = environ.get("DEFAULT_COLOR", "fff").strip() DEFAULT_COLOR = environ.get("DEFAULT_COLOR", "fff").strip()
COLORS = {'ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','eb4963','ff0000','f39731','30409f','3e98a7','e4432d','7b9ae4','ec72de','7f8fa6', 'f8db58','8cdbe6', DEFAULT_COLOR} COLORS = {'ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','eb4963','ff0000','f39731','30409f','3e98a7','e4432d','7b9ae4','ec72de','7f8fa6', 'f8db58','8cdbe6', DEFAULT_COLOR}
LOGGEDIN_ACTIVE_TIME = 15 * 60
AWARDS = { AWARDS = {
"ghost": { "ghost": {
"kind": "ghost", "kind": "ghost",

View file

@ -3,13 +3,19 @@ from .alerts import *
from files.helpers.const import * from files.helpers.const import *
from files.__main__ import db_session from files.__main__ import db_session
from random import randint from random import randint
import user_agents
import time
def get_logged_in_user(): def get_logged_in_user():
if not (hasattr(g, 'db') and g.db): g.db = db_session() if hasattr(g, 'v'):
return g.v
if not (hasattr(g, 'db') and g.db):
g.db = db_session()
v = None v = None
token = request.headers.get("Authorization","").strip() token = request.headers.get("Authorization", "").strip()
if token: if token:
client = g.db.query(ClientAuth).filter(ClientAuth.access_token == token).one_or_none() client = g.db.query(ClientAuth).filter(ClientAuth.access_token == token).one_or_none()
if client: if client:
@ -19,7 +25,7 @@ def get_logged_in_user():
lo_user = session.get("lo_user") lo_user = session.get("lo_user")
if lo_user: if lo_user:
id = int(lo_user) id = int(lo_user)
v = g.db.query(User).filter_by(id=id).one_or_none() v = g.db.query(User).get(id)
if v: if v:
nonce = session.get("login_nonce", 0) nonce = session.get("login_nonce", 0)
if nonce < v.login_nonce or v.id != id: abort(401) if nonce < v.login_nonce or v.id != id: abort(401)
@ -35,14 +41,40 @@ def get_logged_in_user():
v.client = None v.client = None
if request.method.lower() != "get" \
if request.method.lower() != "get" and app.config['SETTINGS']['Read-only mode'] and not (v and v.admin_level): and app.config['SETTINGS']['Read-only mode'] \
and not (v and v.admin_level):
abort(403) abort(403)
if v and v.patron: if request.content_length and request.content_length > 8 * 1024 * 1024:
if request.content_length and request.content_length > 16 * 1024 * 1024: abort(413) abort(413)
elif request.content_length and request.content_length > 8 * 1024 * 1024: abort(413)
if not session.get("session_id"):
session.permanent = True
session["session_id"] = secrets.token_hex(49)
# Active User Counters
loggedin = cache.get(f'{SITE}_loggedin') or {}
loggedout = cache.get(f'{SITE}_loggedout') or {}
timestamp = int(time.time())
if v:
if session["session_id"] in loggedout:
del loggedout[session["session_id"]]
loggedin[v.id] = timestamp
else:
ua = str(user_agents.parse(g.agent))
if 'spider' not in ua.lower() and 'bot' not in ua.lower():
loggedout[session["session_id"]] = (timestamp, ua)
g.loggedin_counter = len([x for x in loggedin.values() \
if (timestamp - x) < LOGGEDIN_ACTIVE_TIME])
g.loggedout_counter = len([x for x in loggedout.values() \
if (timestamp - x[0]) < LOGGEDIN_ACTIVE_TIME])
cache.set(f'{SITE}_loggedin', loggedin)
cache.set(f'{SITE}_loggedout', loggedout)
g.v = v
return v return v
def check_ban_evade(v): def check_ban_evade(v):
@ -53,12 +85,10 @@ def check_ban_evade(v):
def auth_desired(f): def auth_desired(f):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
v = get_logged_in_user() v = get_logged_in_user()
check_ban_evade(v) check_ban_evade(v)
g.v = v
return make_response(f(*args, v=v, **kwargs)) return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__ wrapper.__name__ = f.__name__
@ -68,13 +98,11 @@ def auth_desired(f):
def auth_required(f): def auth_required(f):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
v = get_logged_in_user() v = get_logged_in_user()
if not v: abort(401) if not v: abort(401)
check_ban_evade(v) check_ban_evade(v)
g.v = v
return make_response(f(*args, v=v, **kwargs)) return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__ wrapper.__name__ = f.__name__
@ -84,9 +112,7 @@ def auth_required(f):
def is_not_permabanned(f): def is_not_permabanned(f):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
v = get_logged_in_user() v = get_logged_in_user()
if not v: abort(401) if not v: abort(401)
check_ban_evade(v) check_ban_evade(v)
@ -94,7 +120,6 @@ def is_not_permabanned(f):
if v.is_suspended_permanently: if v.is_suspended_permanently:
return {"error": "Forbidden: you are permabanned."}, 403 return {"error": "Forbidden: you are permabanned."}, 403
g.v = v
return make_response(f(*args, v=v, **kwargs)) return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__ wrapper.__name__ = f.__name__
@ -106,14 +131,11 @@ def admin_level_required(x):
def wrapper_maker(f): def wrapper_maker(f):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
v = get_logged_in_user() v = get_logged_in_user()
if not v: abort(401) if not v: abort(401)
if v.admin_level < x: abort(403) if v.admin_level < x: abort(403)
g.v = v
return make_response(f(*args, v=v, **kwargs)) return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__ wrapper.__name__ = f.__name__

View file

@ -764,6 +764,25 @@ def users_list(v):
page=page, page=page,
) )
@app.get('/admin/loggedin')
@admin_level_required(2)
def loggedin_list(v):
ids = [x for x, val in cache.get(f'{SITE}_loggedin').items() \
if (time.time() - val) < LOGGEDIN_ACTIVE_TIME]
users = g.db.query(User).filter(User.id.in_(ids)) \
.order_by(User.admin_level.desc(), User.truecoins.desc()).all()
return render_template("admin/loggedin.html", v=v, users=users)
@app.get('/admin/loggedout')
@admin_level_required(2)
def loggedout_list(v):
users = sorted([val[1] for x, val in cache.get(f'{SITE}_loggedout').items() \
if (time.time() - val[0]) < LOGGEDIN_ACTIVE_TIME])
return render_template("admin/loggedout.html", v=v, users=users)
@app.get("/admin/alt_votes") @app.get("/admin/alt_votes")
@admin_level_required(2) @admin_level_required(2)
def alt_votes_get(v): def alt_votes_get(v):

View file

@ -8,7 +8,13 @@
{% block content %} {% block content %}
<pre></pre> <pre></pre>
<pre></pre> <pre></pre>
<h3>&nbsp;Admin Tools</h3> <h3>Admin Tools</h3>
<div class="mb-3">
<strong>Users Here Now:</strong> {{g.loggedin_counter + g.loggedout_counter}} &mdash;
<a href="/admin/loggedin">{{g.loggedin_counter}} Logged-In</a> |
<a href="/admin/loggedout">{{g.loggedout_counter}} Logged-Out</a>
</div>
<h4>Content</h4> <h4>Content</h4>
<ul> <ul>

View file

@ -0,0 +1,21 @@
{% extends "settings2.html" %}
{% block content %}
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Truescore</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.name_color}}" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.name_color}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.truecoins}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends "settings2.html" %}
{% block content %}
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Details</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td>{{loop.index}}</td>
<td>{{user}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -21,6 +21,7 @@ qrcode
redis redis
requests requests
SQLAlchemy SQLAlchemy
user-agents
psycopg2-binary psycopg2-binary
pusher_push_notifications pusher_push_notifications
pyenchant pyenchant