refactor(decorators): simplify require_admin decorator by reusing require_role

This commit is contained in:
JSC
2025-07-04 19:13:33 +02:00
parent 5c29fa1a4c
commit 4375718c2f
3 changed files with 107 additions and 92 deletions

View File

@@ -1,10 +1,9 @@
"""Sound played tracking model.""" """Sound played tracking model."""
from datetime import datetime from datetime import datetime
from typing import List, Optional
from app.database import db from app.database import db
from sqlalchemy import DateTime, ForeignKey, Integer, String from sqlalchemy import DateTime, ForeignKey, Integer, func, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -17,16 +16,15 @@ class SoundPlayed(db.Model):
# Foreign keys # Foreign keys
user_id: Mapped[int] = mapped_column( user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id"), nullable=False Integer, ForeignKey("users.id"), nullable=False,
) )
sound_id: Mapped[int] = mapped_column( sound_id: Mapped[int] = mapped_column(
Integer, ForeignKey("sounds.id"), nullable=False Integer, ForeignKey("sounds.id"), nullable=False,
) )
# Timestamp # Timestamp
played_at: Mapped[datetime] = mapped_column( played_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, nullable=False DateTime, default=datetime.utcnow, nullable=False,
) )
# Relationships # Relationships
@@ -34,8 +32,11 @@ class SoundPlayed(db.Model):
sound: Mapped["Sound"] = relationship("Sound", backref="play_history") sound: Mapped["Sound"] = relationship("Sound", backref="play_history")
def __repr__(self) -> str: def __repr__(self) -> str:
"""String representation of SoundPlayed.""" """Return string representation of SoundPlayed."""
return f"<SoundPlayed user_id={self.user_id} sound_id={self.sound_id} at={self.played_at}>" return (
f"<SoundPlayed user_id={self.user_id} sound_id={self.sound_id} "
f"at={self.played_at}>"
)
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert sound played record to dictionary.""" """Convert sound played record to dictionary."""
@@ -62,6 +63,7 @@ class SoundPlayed(db.Model):
cls, cls,
user_id: int, user_id: int,
sound_id: int, sound_id: int,
*,
commit: bool = True, commit: bool = True,
) -> "SoundPlayed": ) -> "SoundPlayed":
"""Create a new sound played record.""" """Create a new sound played record."""
@@ -77,8 +79,8 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_user_plays( def get_user_plays(
cls, user_id: int, limit: int = 50, offset: int = 0 cls, user_id: int, limit: int = 50, offset: int = 0,
) -> List["SoundPlayed"]: ) -> list["SoundPlayed"]:
"""Get recent plays for a specific user.""" """Get recent plays for a specific user."""
return ( return (
cls.query.filter_by(user_id=user_id) cls.query.filter_by(user_id=user_id)
@@ -90,8 +92,8 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_sound_plays( def get_sound_plays(
cls, sound_id: int, limit: int = 50, offset: int = 0 cls, sound_id: int, limit: int = 50, offset: int = 0,
) -> List["SoundPlayed"]: ) -> list["SoundPlayed"]:
"""Get recent plays for a specific sound.""" """Get recent plays for a specific sound."""
return ( return (
cls.query.filter_by(sound_id=sound_id) cls.query.filter_by(sound_id=sound_id)
@@ -103,8 +105,8 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_recent_plays( def get_recent_plays(
cls, limit: int = 100, offset: int = 0 cls, limit: int = 100, offset: int = 0,
) -> List["SoundPlayed"]: ) -> list["SoundPlayed"]:
"""Get recent plays across all users and sounds.""" """Get recent plays across all users and sounds."""
return ( return (
cls.query.order_by(cls.played_at.desc()) cls.query.order_by(cls.played_at.desc())
@@ -125,10 +127,10 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_popular_sounds( def get_popular_sounds(
cls, limit: int = 10, days: int | None = None cls, limit: int = 10, days: int | None = None,
) -> List[dict]: ) -> list[dict]:
"""Get most popular sounds with play counts.""" """Get most popular sounds with play counts."""
from sqlalchemy import func, text from app.models.sound import Sound
query = ( query = (
db.session.query( db.session.query(
@@ -150,14 +152,16 @@ class SoundPlayed(db.Model):
# Get sound details # Get sound details
popular_sounds = [] popular_sounds = []
for result in results: for result in results:
from app.models.sound import Sound
sound = Sound.query.get(result.sound_id) sound = Sound.query.get(result.sound_id)
if sound: if sound:
popular_sounds.append({ popular_sounds.append({
"sound": sound.to_dict(), "sound": sound.to_dict(),
"play_count": result.play_count, "play_count": result.play_count,
"last_played": result.last_played.isoformat() if result.last_played else None, "last_played": (
result.last_played.isoformat()
if result.last_played
else None
),
}) })
return popular_sounds return popular_sounds
@@ -165,7 +169,7 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_user_stats(cls, user_id: int) -> dict: def get_user_stats(cls, user_id: int) -> dict:
"""Get comprehensive stats for a user.""" """Get comprehensive stats for a user."""
from sqlalchemy import func from app.models.sound import Sound
total_plays = cls.query.filter_by(user_id=user_id).count() total_plays = cls.query.filter_by(user_id=user_id).count()
@@ -199,8 +203,6 @@ class SoundPlayed(db.Model):
favorite_sound = None favorite_sound = None
if favorite_query: if favorite_query:
from app.models.sound import Sound
sound = Sound.query.get(favorite_query.sound_id) sound = Sound.query.get(favorite_query.sound_id)
if sound: if sound:
favorite_sound = { favorite_sound = {
@@ -224,6 +226,10 @@ class SoundPlayed(db.Model):
"total_plays": total_plays, "total_plays": total_plays,
"unique_sounds": unique_sounds, "unique_sounds": unique_sounds,
"favorite_sound": favorite_sound, "favorite_sound": favorite_sound,
"first_play": first_play.played_at.isoformat() if first_play else None, "first_play": (
"last_play": last_play.played_at.isoformat() if last_play else None, first_play.played_at.isoformat() if first_play else None
),
"last_play": (
last_play.played_at.isoformat() if last_play else None
),
} }

View File

@@ -1,10 +1,15 @@
"""Soundboard routes.""" """Soundboard routes."""
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from app.models.sound import Sound, SoundType from app.models.sound import Sound, SoundType
from app.models.sound_played import SoundPlayed from app.models.sound_played import SoundPlayed
from app.services.decorators import (
get_current_user,
require_auth,
require_credits,
)
from app.services.vlc_service import vlc_service from app.services.vlc_service import vlc_service
from app.services.decorators import require_auth, get_current_user
bp = Blueprint("soundboard", __name__, url_prefix="/api/soundboard") bp = Blueprint("soundboard", __name__, url_prefix="/api/soundboard")
@@ -40,6 +45,7 @@ def get_sounds():
@bp.route("/sounds/<int:sound_id>/play", methods=["POST"]) @bp.route("/sounds/<int:sound_id>/play", methods=["POST"])
@require_auth @require_auth
@require_credits(1)
def play_sound(sound_id: int): def play_sound(sound_id: int):
"""Play a specific sound.""" """Play a specific sound."""
try: try:
@@ -61,9 +67,10 @@ def play_sound(sound_id: int):
if success: if success:
return jsonify({"message": "Sound playing", "sound_id": sound_id}) return jsonify({"message": "Sound playing", "sound_id": sound_id})
else: else:
return jsonify( return (
{"error": "Sound not found or cannot be played"} jsonify({"error": "Sound not found or cannot be played"}),
), 404 404,
)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@@ -78,15 +85,18 @@ def stop_all_sounds():
# Wait a moment and check if any are still playing # Wait a moment and check if any are still playing
import time import time
time.sleep(0.2) time.sleep(0.2)
# If there are still instances, force stop them # If there are still instances, force stop them
if vlc_service.get_playing_count() > 0: if vlc_service.get_playing_count() > 0:
stopped_count = vlc_service.force_stop_all() stopped_count = vlc_service.force_stop_all()
return jsonify({ return jsonify(
{
"message": f"Force stopped {stopped_count} sounds", "message": f"Force stopped {stopped_count} sounds",
"forced": True "forced": True,
}) }
)
return jsonify({"message": "All sounds stopped"}) return jsonify({"message": "All sounds stopped"})
except Exception as e: except Exception as e:
@@ -99,10 +109,12 @@ def force_stop_all_sounds():
"""Force stop all sounds with aggressive cleanup.""" """Force stop all sounds with aggressive cleanup."""
try: try:
stopped_count = vlc_service.force_stop_all() stopped_count = vlc_service.force_stop_all()
return jsonify({ return jsonify(
{
"message": f"Force stopped {stopped_count} sound instances", "message": f"Force stopped {stopped_count} sound instances",
"stopped_count": stopped_count "stopped_count": stopped_count,
}) }
)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@@ -118,11 +130,13 @@ def get_status():
with vlc_service.lock: with vlc_service.lock:
processes = [] processes = []
for process_id, process in vlc_service.processes.items(): for process_id, process in vlc_service.processes.items():
processes.append({ processes.append(
{
"id": process_id, "id": process_id,
"pid": process.pid, "pid": process.pid,
"running": process.poll() is None, "running": process.poll() is None,
}) }
)
return jsonify( return jsonify(
{ {
@@ -144,13 +158,17 @@ def get_play_history():
per_page = min(int(request.args.get("per_page", 50)), 100) per_page = min(int(request.args.get("per_page", 50)), 100)
offset = (page - 1) * per_page offset = (page - 1) * per_page
recent_plays = SoundPlayed.get_recent_plays(limit=per_page, offset=offset) recent_plays = SoundPlayed.get_recent_plays(
limit=per_page, offset=offset
)
return jsonify({ return jsonify(
{
"plays": [play.to_dict() for play in recent_plays], "plays": [play.to_dict() for play in recent_plays],
"page": page, "page": page,
"per_page": per_page, "per_page": per_page,
}) }
)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@@ -169,14 +187,18 @@ def get_my_play_history():
per_page = min(int(request.args.get("per_page", 50)), 100) per_page = min(int(request.args.get("per_page", 50)), 100)
offset = (page - 1) * per_page offset = (page - 1) * per_page
user_plays = SoundPlayed.get_user_plays(user_id=user_id, limit=per_page, offset=offset) user_plays = SoundPlayed.get_user_plays(
user_id=user_id, limit=per_page, offset=offset
)
return jsonify({ return jsonify(
{
"plays": [play.to_dict() for play in user_plays], "plays": [play.to_dict() for play in user_plays],
"page": page, "page": page,
"per_page": per_page, "per_page": per_page,
"user_id": user_id, "user_id": user_id,
}) }
)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@@ -209,10 +231,12 @@ def get_popular_sounds():
popular_sounds = SoundPlayed.get_popular_sounds(limit=limit, days=days) popular_sounds = SoundPlayed.get_popular_sounds(limit=limit, days=days)
return jsonify({ return jsonify(
{
"popular_sounds": popular_sounds, "popular_sounds": popular_sounds,
"limit": limit, "limit": limit,
"days": days, "days": days,
}) }
)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500

View File

@@ -148,22 +148,7 @@ def require_role(required_role: str):
def require_admin(f): def require_admin(f):
"""Decorator to require admin role for routes.""" """Decorator to require admin role for routes."""
return require_role("admin")(f)
@wraps(f)
def wrapper(*args, **kwargs):
user = get_current_user()
if not user:
return jsonify({"error": "Authentication required"}), 401
if user.get("role") != "admin":
return (
jsonify({"error": "Access denied. Admin role required"}),
403,
)
return f(*args, **kwargs)
return wrapper
def require_credits(credits_needed: int): def require_credits(credits_needed: int):