From 83fe4b9013446aa547fa6d9e8086bab92891fa4b Mon Sep 17 00:00:00 2001 From: Jonny Ervine Date: Sat, 4 Oct 2025 21:05:01 +0800 Subject: [PATCH] VArious admin fixes --- motm_app/main.py | 151 +++++++++++++++- motm_app/templates/admin_dashboard.html | 8 + motm_app/templates/device_tracking.html | 230 ++++++++++++++++++++++++ motm_app/templates/index.html | 4 + 4 files changed, 386 insertions(+), 7 deletions(-) create mode 100644 motm_app/templates/device_tracking.html diff --git a/motm_app/main.py b/motm_app/main.py index 0309a1f..193c460 100644 --- a/motm_app/main.py +++ b/motm_app/main.py @@ -66,7 +66,9 @@ class AdminProfileForm(FlaskForm): @app.route('/') def index(): """Main index page for MOTM system""" - return render_template('index.html') + # Check if user is authenticated as admin + is_admin = is_admin_authenticated(request) + return render_template('index.html', is_admin=is_admin) @app.route('/admin') @@ -127,6 +129,54 @@ def admin_profile(): return render_template('admin_profile.html', form=form, current_email=current_email) +def generate_device_id(request): + """Generate a device identifier from request headers""" + import hashlib + + # Collect device characteristics + user_agent = request.headers.get('User-Agent', '') + accept_language = request.headers.get('Accept-Language', '') + accept_encoding = request.headers.get('Accept-Encoding', '') + ip_address = request.environ.get('REMOTE_ADDR', '') + + # Create a fingerprint from these characteristics + fingerprint_string = f"{user_agent}|{accept_language}|{accept_encoding}|{ip_address}" + device_id = hashlib.sha256(fingerprint_string.encode()).hexdigest()[:16] # Use first 16 chars + + return device_id + + +def is_admin_authenticated(request): + """Check if the current request is authenticated as admin""" + try: + # Check if Authorization header exists + auth_header = request.headers.get('Authorization', '') + if not auth_header.startswith('Basic '): + return False + + # Decode the basic auth credentials + import base64 + encoded_credentials = auth_header[6:] # Remove 'Basic ' prefix + try: + credentials = base64.b64decode(encoded_credentials).decode('utf-8') + username, password = credentials.split(':', 1) + + # Check against database + sql = text("SELECT password_hash FROM admin_profiles WHERE username = :username") + result = sql_read(sql, {'username': username}) + + if result: + stored_hash = result[0]['password_hash'] + password_hash = hashlib.sha256(password.encode()).hexdigest() + return password_hash == stored_hash + except: + pass + except: + pass + + return False + + # ==================== PUBLIC VOTING SECTION ==================== @app.route('/motm/') @@ -206,16 +256,16 @@ def motm_vote(randomUrlSuffix): @app.route('/motm/comments', methods=['GET', 'POST']) def match_comments(): """Display and allow adding match comments""" - sql = text("SELECT nextClub, nextTeam, nextdate, oppoLogo, hkfcLogo FROM motmadminsettings") + sql = text("SELECT nextclub, nextteam, nextdate, oppologo, hkfclogo FROM motmadminsettings") row = sql_read_static(sql) if not row: return render_template('error.html', message="Database not initialized. Please go to Database Setup to initialize the database.") - _oppo = row[0]['nextClub'] + _oppo = row[0]['nextclub'] commentDate = row[0]['nextdate'].strftime('%Y-%m-%d') _matchDate = row[0]['nextdate'].strftime('%Y_%m_%d') - hkfcLogo = row[0]['hkfcLogo'] - oppoLogo = row[0]['oppoLogo'] + hkfcLogo = row[0]['hkfclogo'] + oppoLogo = row[0]['oppologo'] if request.method == 'POST': _comment = request.form['matchComment'] if _comment != 'Optional comments added here': @@ -282,6 +332,25 @@ def vote_thanks(): """) sql_write(sql_dotd, {'player_num': _dotd, 'player_name': dotd_name}) + # Generate device identifier and record vote for tracking + device_id = generate_device_id(request) + sql_device = text(""" + INSERT INTO device_votes (device_id, fixture_date, motm_player_number, dotd_player_number, + motm_player_name, dotd_player_name, ip_address, user_agent) + VALUES (:device_id, :fixture_date, :motm_player, :dotd_player, + :motm_name, :dotd_name, :ip_address, :user_agent) + """) + sql_write(sql_device, { + 'device_id': device_id, + 'fixture_date': fixture_date, + 'motm_player': _motm, + 'dotd_player': _dotd, + 'motm_name': motm_name, + 'dotd_name': dotd_name, + 'ip_address': request.environ.get('REMOTE_ADDR', 'unknown'), + 'user_agent': request.headers.get('User-Agent', 'unknown') + }) + # Handle comments if _comments and _comments != "Optional comments added here": sql3 = text("INSERT INTO _motmcomments (matchDate, comment) VALUES (:match_date, :comment)") @@ -338,7 +407,7 @@ def motm_admin(): # Only update currMotM and currDotD if they were provided if _currMotM and _currMotM != '0' and _currDotD and _currDotD != '0': - sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextClub = :next_club, nextTeam = :next_team, currMotM = :curr_motm, currDotD = :curr_dotd") + sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextclub = :next_club, nextteam = :next_team, currmotm = :curr_motm, currdotd = :curr_dotd") sql_write_static(sql, { 'next_date': _nextMatchDate, 'next_club': _nextClub, @@ -348,7 +417,7 @@ def motm_admin(): }) else: # Don't update currMotM and currDotD if not provided - sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextClub = :next_club, nextTeam = :next_team") + sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextclub = :next_club, nextteam = :next_team") sql_write_static(sql, { 'next_date': _nextMatchDate, 'next_club': _nextClub, @@ -993,6 +1062,74 @@ def club_selection(): return render_template('club_selection.html', form=form, clubs=clubs, selected_clubs=[]) +@app.route('/admin/device-tracking', methods=['GET', 'POST']) +@basic_auth.required +def device_tracking(): + """Admin page for viewing device voting patterns""" + + if request.method == 'POST': + action = request.form.get('action') + + if action == 'analyze_patterns': + # Analyze voting patterns by device + sql_patterns = text(""" + SELECT + device_id, + COUNT(*) as vote_count, + COUNT(DISTINCT fixture_date) as fixtures_voted, + STRING_AGG(DISTINCT motm_player_name, ', ') as motm_players, + STRING_AGG(DISTINCT dotd_player_name, ', ') as dotd_players, + MIN(vote_timestamp) as first_vote, + MAX(vote_timestamp) as last_vote, + STRING_AGG(DISTINCT ip_address::text, ', ') as ip_addresses + FROM device_votes + GROUP BY device_id + HAVING COUNT(*) > 1 + ORDER BY vote_count DESC, fixtures_voted DESC + """) + patterns = sql_read(sql_patterns) + + return render_template('device_tracking.html', patterns=patterns, analysis_mode=True) + + elif action == 'view_device_details': + device_id = request.form.get('device_id') + if device_id: + sql_details = text(""" + SELECT + device_id, + fixture_date, + motm_player_name, + dotd_player_name, + ip_address, + user_agent, + vote_timestamp + FROM device_votes + WHERE device_id = :device_id + ORDER BY vote_timestamp DESC + """) + device_details = sql_read(sql_details, {'device_id': device_id}) + + return render_template('device_tracking.html', device_details=device_details, + selected_device=device_id, details_mode=True) + + # Default view - show recent votes + sql_recent = text(""" + SELECT + device_id, + fixture_date, + motm_player_name, + dotd_player_name, + ip_address, + vote_timestamp + FROM device_votes + ORDER BY vote_timestamp DESC + LIMIT 50 + """) + recent_votes = sql_read(sql_recent) + + return render_template('device_tracking.html', recent_votes=recent_votes) + + @app.route('/admin/motm/manage', methods=['GET', 'POST']) @basic_auth.required def motm_management(): diff --git a/motm_app/templates/admin_dashboard.html b/motm_app/templates/admin_dashboard.html index c16cb20..38e596a 100644 --- a/motm_app/templates/admin_dashboard.html +++ b/motm_app/templates/admin_dashboard.html @@ -153,6 +153,14 @@ +
diff --git a/motm_app/templates/device_tracking.html b/motm_app/templates/device_tracking.html new file mode 100644 index 0000000..aac6633 --- /dev/null +++ b/motm_app/templates/device_tracking.html @@ -0,0 +1,230 @@ + + + + + + Device Tracking - HKFC Men's C Team MOTM System + + + + +
+
+
+

Device Tracking Analysis

+

Monitor voting patterns and detect potential duplicate voting

+ +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + +
+

Analysis Controls

+
+
+
+ + +
+ Find devices with multiple votes +
+
+ View Recent Votes + Show last 50 votes +
+
+
+ + + {% if analysis_mode and patterns %} +
+

Voting Pattern Analysis

+

Devices that have voted multiple times:

+ + {% if patterns %} +
+ + + + + + + + + + + + + + + + {% for pattern in patterns %} + + + + + + + + + + + + {% endfor %} + +
Device IDVote CountFixturesMOTM PlayersDotD PlayersFirst VoteLast VoteIP AddressesActions
{{ pattern.device_id }} + + {{ pattern.vote_count }} + + {{ pattern.fixtures_voted }}{{ pattern.motm_players }}{{ pattern.dotd_players }}{{ pattern.first_vote.strftime('%Y-%m-%d %H:%M') if pattern.first_vote else 'N/A' }}{{ pattern.last_vote.strftime('%Y-%m-%d %H:%M') if pattern.last_vote else 'N/A' }}{{ pattern.ip_addresses }} +
+ + + +
+
+
+ + {% if patterns|length > 0 %} +
+
Pattern Analysis
+

+ Warning: {{ patterns|length }} device(s) have voted multiple times. + This could indicate duplicate voting or shared devices. +

+
+ {% endif %} + {% else %} +
+
No Suspicious Patterns Found
+

All devices have voted only once per fixture.

+
+ {% endif %} +
+ {% endif %} + + + {% if details_mode and device_details %} +
+

Device Details: {{ selected_device }}

+

Complete voting history for this device:

+ +
+ + + + + + + + + + + + {% for vote in device_details %} + + + + + + + + {% endfor %} + +
Fixture DateMOTM VoteDotD VoteIP AddressVote Time
{{ vote.fixture_date }}{{ vote.motm_player_name }}{{ vote.dotd_player_name }}{{ vote.ip_address }}{{ vote.vote_timestamp.strftime('%Y-%m-%d %H:%M:%S') if vote.vote_timestamp else 'N/A' }}
+
+ +
+
Device Information
+ {% if device_details %} +

User Agent: {{ device_details[0].user_agent[:100] }}{% if device_details[0].user_agent|length > 100 %}...{% endif %}

+

Total Votes: {{ device_details|length }}

+

Unique Fixtures: {{ device_details|map(attribute='fixture_date')|unique|list|length }}

+ {% endif %} +
+
+ {% endif %} + + + {% if recent_votes and not analysis_mode and not details_mode %} +
+

Recent Votes

+

Last 50 votes cast:

+ +
+ + + + + + + + + + + + + {% for vote in recent_votes %} + + + + + + + + + {% endfor %} + +
Device IDFixture DateMOTM VoteDotD VoteIP AddressVote Time
{{ vote.device_id }}{{ vote.fixture_date }}{{ vote.motm_player_name }}{{ vote.dotd_player_name }}{{ vote.ip_address }}{{ vote.vote_timestamp.strftime('%Y-%m-%d %H:%M') if vote.vote_timestamp else 'N/A' }}
+
+
+ {% endif %} + + + {% if not recent_votes and not patterns and not device_details %} +
+
No Vote Data Available
+

No votes have been cast yet, or the device tracking table is empty.

+
+ {% endif %} +
+
+
+ + + + diff --git a/motm_app/templates/index.html b/motm_app/templates/index.html index bfa6b90..f2cfa01 100644 --- a/motm_app/templates/index.html +++ b/motm_app/templates/index.html @@ -31,6 +31,10 @@