Add admin maintenance console
Admin-only page reachable from a new card on the admin panel (replacing the Quick Actions card) that centralises housekeeping operations: * Database integrity: PRAGMA integrity_check / quick_check / foreign_key_check, plus VACUUM + ANALYZE to reclaim free space. * Leftover records: detects and removes orphaned rows left behind by deleted players/content (player_feedback, player_edit, playlist_content, dangling player.playlist_id) and orphaned edited-media folders on disk. * Player authentication tokens: manual reset (single or all), a configurable automatic reset interval, and cancellation of a pending reset. Resetting a token rotates player.auth_code and sets auth_reset_required, so the next heartbeat (POST /api/player-feedback) answers 401 + auth_required and the client restarts its authentication flow. A successful authentication clears the flag and stamps auth_token_issued_at, from which the interval is measured. New: PlayerAuthConfig model, add_player_auth_reset migration (registered in docker-entrypoint.sh), admin/maintenance.html.
This commit is contained in:
+67
-1
@@ -107,6 +107,47 @@ def get_assigned_playlist(player: Player) -> Optional[Playlist]:
|
||||
return db.session.get(Playlist, player.playlist_id)
|
||||
|
||||
|
||||
def player_needs_reauth(player: Player):
|
||||
"""Decide whether a heartbeat should force the player to re-authenticate.
|
||||
|
||||
Two independent triggers are combined:
|
||||
|
||||
* an explicit reset flag (``auth_reset_required``) set from the admin
|
||||
maintenance page, individually or for every player; and
|
||||
* the optional automatic expiry interval configured on that page — a token
|
||||
older than ``reset_interval_days`` is rotated and the player is asked to
|
||||
authenticate again.
|
||||
|
||||
Args:
|
||||
player: The player sending the heartbeat.
|
||||
|
||||
Returns:
|
||||
``(needs_reauth, reason)`` where reason is one of ``'token_reset'``,
|
||||
``'token_expired'`` or ``None``.
|
||||
"""
|
||||
from app.models import PlayerAuthConfig
|
||||
|
||||
if getattr(player, 'auth_reset_required', False):
|
||||
return True, 'token_reset'
|
||||
|
||||
interval_days = PlayerAuthConfig.get_interval_days()
|
||||
if interval_days <= 0:
|
||||
return False, None
|
||||
|
||||
issued = getattr(player, 'auth_token_issued_at', None)
|
||||
if not issued:
|
||||
return False, None
|
||||
|
||||
if datetime.utcnow() - issued >= timedelta(days=interval_days):
|
||||
# Expire the token now: rotate it and flag the player, so the response
|
||||
# below tells the client to restart authentication.
|
||||
player.rotate_auth_token(force_reauth=True)
|
||||
db.session.commit()
|
||||
return True, 'token_expired'
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
@api_bp.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""API health check endpoint."""
|
||||
@@ -158,7 +199,11 @@ def authenticate_player():
|
||||
log_action('warning', f'Failed authentication attempt for hostname: {hostname}')
|
||||
return jsonify({'error': 'Invalid credentials'}), 401
|
||||
|
||||
# Update player status
|
||||
# Update player status and stamp the token. Marking the player as
|
||||
# authenticated clears any pending re-authentication request set from the
|
||||
# admin maintenance page, so a player that just re-authenticated stops
|
||||
# being told to authenticate again.
|
||||
player.mark_authenticated()
|
||||
player.update_status('online')
|
||||
db.session.commit()
|
||||
|
||||
@@ -508,6 +553,27 @@ def receive_player_feedback():
|
||||
log_action('warning', f'Invalid quickconnect in feedback from: {player.name} ({player.hostname})')
|
||||
return jsonify({'error': 'Invalid quickconnect code'}), 403
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Authentication-token reset.
|
||||
#
|
||||
# When the admin maintenance page resets a player's token (or an
|
||||
# automatic reset interval has elapsed), the heartbeat is answered with
|
||||
# 401 + auth_required so the client restarts its authentication flow.
|
||||
# This runs before any feedback is recorded: the heartbeat is not a
|
||||
# meaningful status update while the client is being asked to
|
||||
# re-authenticate.
|
||||
# ------------------------------------------------------------------
|
||||
needs_reauth, reauth_reason = player_needs_reauth(player)
|
||||
if needs_reauth:
|
||||
log_action('info', f'Re-authentication required for {player.name} '
|
||||
f'({player.hostname}): {reauth_reason}')
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'auth_required': True,
|
||||
'reason': reauth_reason,
|
||||
'message': 'Authentication token reset. Please re-authenticate.',
|
||||
}), 401
|
||||
|
||||
# Create feedback record
|
||||
status = data.get('status', 'unknown')
|
||||
message = data.get('message', '')
|
||||
|
||||
Reference in New Issue
Block a user