Files
KiraTV/backend/api/admin/users.py
T
KiraStream b969b7e5af Mejoras de estabilidad de stream IPTV y nuevo registro de fallos
- restream.py: eliminar -reconnect_streamed de ffmpeg para evitar rebobinados
  al reconectar (el proveedor re-enviaba desde keyframe anterior causando PTS
  backward). El loop de Python gestiona las reconexiones con conexión fresca.
- restream.py: eliminar _drain_queue del loop de reconexión para que el buffer
  de la cola cubra el tiempo de reconexión sin pantalla negra en el player.
- restream.py: añadir buffer_server_bytes/secs/capacity a stats() y método
  _log_stream_event() para persistir eventos de caída y recuperación en BD.
- models/stream_event.py: nuevo modelo StreamEvent para registro de fallos.
- database.py: registrar StreamEvent en init_db.
- api/admin/logs.py: nuevos endpoints GET/DELETE /logs/events.
- Dashboard.tsx: mostrar reserva de buffer del servidor (capacidad + estado).
- Logs.tsx: añadir pestaña "Fallos de stream" con tabla de eventos persistidos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:43:31 +00:00

268 lines
9.2 KiB
Python

"""Admin API: user management."""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from pydantic import BaseModel
from ...database import get_db
from ...models.user import User, UserCatalogEntry, UserCustomCatalogEntry, UserProviderPreference
from ...models.channel import Category
from ...models.provider import ProviderAccount
from ...models.custom_category import CustomCategory
from ..auth import get_current_admin
router = APIRouter(prefix="/users", tags=["users"])
class UserCreate(BaseModel):
username: str
password: str
max_connections: int = 1
expiry_date: datetime | None = None
is_active: bool = True
is_priority: bool = False
class UserUpdate(BaseModel):
password: str | None = None
max_connections: int | None = None
expiry_date: datetime | None = None
is_active: bool | None = None
is_priority: bool | None = None
class UserOut(BaseModel):
id: int
username: str
max_connections: int
expiry_date: datetime | None
is_active: bool
is_priority: bool
created_at: datetime
class Config:
from_attributes = True
@router.get("/", response_model=list[UserOut])
async def list_users(db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(select(User).order_by(User.id))
return result.scalars().all()
@router.post("/", response_model=UserOut)
async def create_user(data: UserCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
existing = await db.execute(select(User).where(User.username == data.username))
if existing.scalar_one_or_none():
raise HTTPException(400, f"Username '{data.username}' already exists")
user = User(
username=data.username,
max_connections=data.max_connections,
expiry_date=data.expiry_date,
is_active=data.is_active,
is_priority=data.is_priority,
password_hash="",
)
user.set_password(data.password)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@router.put("/{user_id}", response_model=UserOut)
async def update_user(user_id: int, data: UserUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "User not found")
if data.password is not None:
user.set_password(data.password)
if data.max_connections is not None:
user.max_connections = data.max_connections
if data.expiry_date is not None:
user.expiry_date = data.expiry_date
if data.is_active is not None:
user.is_active = data.is_active
if data.is_priority is not None:
user.is_priority = data.is_priority
await db.commit()
await db.refresh(user)
return user
@router.delete("/{user_id}")
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "User not found")
await db.delete(user)
await db.commit()
return {"ok": True}
@router.get("/{user_id}/catalog")
async def get_user_catalog(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(
select(UserCatalogEntry)
.options(selectinload(UserCatalogEntry.category))
.where(UserCatalogEntry.user_id == user_id)
)
entries = result.scalars().all()
return [
{"id": e.id, "category_id": e.category_id, "category_name": e.category.name, "type": e.category.type}
for e in entries
]
@router.post("/{user_id}/catalog")
async def assign_categories(user_id: int, category_ids: list[int], db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(select(User).where(User.id == user_id))
if not result.scalar_one_or_none():
raise HTTPException(404, "User not found")
# Remove existing entries
existing = await db.execute(select(UserCatalogEntry).where(UserCatalogEntry.user_id == user_id))
for entry in existing.scalars().all():
await db.delete(entry)
# Add new entries
added = 0
for cat_id in category_ids:
cat_result = await db.execute(select(Category).where(Category.id == cat_id))
if cat_result.scalar_one_or_none():
db.add(UserCatalogEntry(user_id=user_id, category_id=cat_id))
added += 1
await db.commit()
return {"ok": True, "assigned": added}
@router.delete("/{user_id}/catalog/{category_id}")
async def remove_category(user_id: int, category_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(
select(UserCatalogEntry).where(
UserCatalogEntry.user_id == user_id,
UserCatalogEntry.category_id == category_id,
)
)
entry = result.scalar_one_or_none()
if not entry:
raise HTTPException(404, "Catalog entry not found")
await db.delete(entry)
await db.commit()
return {"ok": True}
@router.get("/{user_id}/custom-catalog")
async def get_user_custom_catalog(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(
select(UserCustomCatalogEntry.custom_category_id).where(UserCustomCatalogEntry.user_id == user_id)
)
return [row[0] for row in result.all()]
@router.post("/{user_id}/custom-catalog")
async def assign_custom_categories(user_id: int, custom_category_ids: list[int], db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(select(User).where(User.id == user_id))
if not result.scalar_one_or_none():
raise HTTPException(404, "User not found")
existing = await db.execute(select(UserCustomCatalogEntry).where(UserCustomCatalogEntry.user_id == user_id))
for entry in existing.scalars().all():
await db.delete(entry)
added = 0
for cc_id in custom_category_ids:
cc_result = await db.execute(select(CustomCategory).where(CustomCategory.id == cc_id))
if cc_result.scalar_one_or_none():
db.add(UserCustomCatalogEntry(user_id=user_id, custom_category_id=cc_id))
added += 1
await db.commit()
return {"ok": True, "assigned": added}
# ---------------------------------------------------------------------------
# Provider preferences
# ---------------------------------------------------------------------------
class ProviderPrefIn(BaseModel):
provider_account_id: int
priority: int = 0
@router.get("/{user_id}/providers")
async def get_user_provider_prefs(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
result = await db.execute(
select(UserProviderPreference)
.options(selectinload(UserProviderPreference.provider_account))
.where(UserProviderPreference.user_id == user_id)
.order_by(UserProviderPreference.priority)
)
prefs = result.scalars().all()
return [
{
"id": p.id,
"provider_account_id": p.provider_account_id,
"provider_name": p.provider_account.name,
"priority": p.priority,
}
for p in prefs
]
@router.post("/{user_id}/providers")
async def add_user_provider_pref(
user_id: int, data: ProviderPrefIn, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)
):
user_result = await db.execute(select(User).where(User.id == user_id))
if not user_result.scalar_one_or_none():
raise HTTPException(404, "User not found")
provider_result = await db.execute(select(ProviderAccount).where(ProviderAccount.id == data.provider_account_id))
if not provider_result.scalar_one_or_none():
raise HTTPException(404, "Provider account not found")
# Upsert
existing = await db.execute(
select(UserProviderPreference).where(
UserProviderPreference.user_id == user_id,
UserProviderPreference.provider_account_id == data.provider_account_id,
)
)
pref = existing.scalar_one_or_none()
if pref:
pref.priority = data.priority
else:
pref = UserProviderPreference(
user_id=user_id,
provider_account_id=data.provider_account_id,
priority=data.priority,
)
db.add(pref)
await db.commit()
return {"ok": True}
@router.delete("/{user_id}/providers/{provider_account_id}")
async def remove_user_provider_pref(
user_id: int, provider_account_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)
):
result = await db.execute(
select(UserProviderPreference).where(
UserProviderPreference.user_id == user_id,
UserProviderPreference.provider_account_id == provider_account_id,
)
)
pref = result.scalar_one_or_none()
if not pref:
raise HTTPException(404, "Preference not found")
await db.delete(pref)
await db.commit()
return {"ok": True}