from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.user import User, CreditTransaction, AdminUser, AdminNotification, LandingFeature, LandingPlatform, LandingTutorial, LandingGalleryItem
from app.models.iglesia import Iglesia
from app.models.lyrics import Group
from app.routers.auth import SECRET_KEY, ALGORITHM, verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, get_password_hash
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime, timedelta
from jose import JWTError, jwt
router = APIRouter(prefix="/admin", tags=["Admin"])
class CreditsUpdate(BaseModel):
credits: int
class PremiumUpdate(BaseModel):
is_premium: bool
class AdminTokenResponse(BaseModel):
access_token: str
token_type: str
expires_in: int
user: dict
class AdminCreate(BaseModel):
username: str
password: str
perm_users: Optional[bool] = True
perm_workers: Optional[bool] = True
perm_churches: Optional[bool] = True
perm_groups: Optional[bool] = True
perm_notifications: Optional[bool] = True
class AdminPasswordUpdate(BaseModel):
password: str
class AdminPermissionsUpdate(BaseModel):
perm_users: bool
perm_workers: bool
perm_churches: bool
perm_groups: bool
perm_notifications: bool
class AdminAvatarUpdate(BaseModel):
avatar_url: str
class NotificationCreate(BaseModel):
title: str
message: str
oauth2_scheme_admin = OAuth2PasswordBearer(tokenUrl="admin/token")
def get_current_admin(
token: str = Depends(oauth2_scheme_admin),
db: Session = Depends(get_db)
) -> AdminUser:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciales de administrador inválidas",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_aud": False})
username: str = payload.get("sub")
role: str = payload.get("role")
if username is None or role != "admin":
raise credentials_exception
except JWTError:
raise credentials_exception
admin = db.query(AdminUser).filter(AdminUser.username == username).first()
if admin is None:
raise credentials_exception
return admin
@router.post("/token", response_model=AdminTokenResponse)
async def login_admin(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
admin = db.query(AdminUser).filter(AdminUser.username == form_data.username).first()
if not admin:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Usuario o contraseña incorrectos",
headers={"WWW-Authenticate": "Bearer"},
)
if not verify_password(form_data.password, admin.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Usuario o contraseña incorrectos",
headers={"WWW-Authenticate": "Bearer"},
)
# Generar token con role="admin"
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
token_payload = {"sub": admin.username, "role": "admin"}
access_token = create_access_token(
data=token_payload, expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
"user": {
"id": admin.id,
"username": admin.username,
"role": "admin",
"avatar_url": admin.avatar_url,
"perm_users": admin.perm_users,
"perm_workers": admin.perm_workers,
"perm_churches": admin.perm_churches,
"perm_groups": admin.perm_groups,
"perm_notifications": admin.perm_notifications
}
}
@router.get("/users")
def list_users(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_users:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar usuarios")
users = db.query(User).all()
return [
{
"id": u.id,
"username": u.username,
"email": u.email,
"credits": u.credits,
"is_premium": u.is_premium
}
for u in users
]
@router.put("/users/{user_id}/credits")
def update_user_credits(user_id: int, data: CreditsUpdate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_users:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar usuarios")
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
old_credits = user.credits
user.credits = data.credits
# Registrar la transacción de crédito
tx = CreditTransaction(
user_id=user.id,
amount=data.credits - old_credits,
concept="Ajuste administrativo de créditos",
created_at=datetime.utcnow()
)
db.add(tx)
db.commit()
db.refresh(user)
# Notificar al usuario por WebSocket en tiempo real
try:
from app.websocket_manager import send_sync_message
send_sync_message({
"type": "user_credits_updated",
"credits": user.credits
}, user.id)
except Exception:
pass
return {
"id": user.id,
"username": user.username,
"credits": user.credits
}
@router.put("/users/{user_id}/premium")
def update_user_premium(user_id: int, data: PremiumUpdate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_users:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar usuarios")
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
user.is_premium = data.is_premium
db.commit()
db.refresh(user)
# Notificar al usuario por WebSocket en tiempo real
try:
from app.websocket_manager import send_sync_message
send_sync_message({
"type": "user_premium_updated",
"is_premium": user.is_premium
}, user.id)
except Exception:
pass
return {
"id": user.id,
"username": user.username,
"is_premium": user.is_premium
}
@router.put("/users/{user_id}/password")
def update_user_password(user_id: int, data: AdminPasswordUpdate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_users:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar usuarios")
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
user.hashed_password = get_password_hash(data.password)
db.commit()
return {"message": "Contraseña de usuario actualizada exitosamente"}
@router.get("/churches")
def list_churches(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_churches:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar iglesias")
churches = db.query(Iglesia).all()
return [
{
"id": c.id,
"nombre": c.nombre,
"pastor": c.pastor,
"codigo": c.codigo,
"admin_id": c.admin_id,
"created_at": c.created_at.isoformat() if c.created_at else None
}
for c in churches
]
@router.get("/groups")
def list_groups(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_groups:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar grupos")
groups = db.query(Group).all()
return [
{
"id": g.id,
"name": g.name,
"codigo": g.codigo,
"created_at": g.created_at.isoformat() if g.created_at else None
}
for g in groups
]
@router.get("/notifications")
def list_notifications(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_notifications:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar notificaciones")
notifications = db.query(AdminNotification).order_by(AdminNotification.created_at.desc()).all()
return [
{
"id": n.id,
"title": n.title,
"message": n.message,
"created_at": n.created_at.isoformat() if n.created_at else None,
"read": n.read
}
for n in notifications
]
@router.put("/notifications/{notif_id}/read")
def mark_notification_read(notif_id: int, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_notifications:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar notificaciones")
notif = db.query(AdminNotification).filter(AdminNotification.id == notif_id).first()
if not notif:
raise HTTPException(status_code=404, detail="Notificación no encontrada")
notif.read = True
db.commit()
return {"message": "Notificación marcada como leída"}
@router.delete("/notifications/{notif_id}")
def delete_notification(notif_id: int, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_notifications:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar notificaciones")
notif = db.query(AdminNotification).filter(AdminNotification.id == notif_id).first()
if not notif:
raise HTTPException(status_code=404, detail="Notificación no encontrada")
db.delete(notif)
db.commit()
return {"message": "Notificación eliminada exitosamente"}
@router.post("/notifications")
def create_notification(data: NotificationCreate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_notifications:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar notificaciones")
notif = AdminNotification(
title=data.title,
message=data.message,
created_at=datetime.utcnow(),
read=False
)
db.add(notif)
db.commit()
db.refresh(notif)
return {
"id": notif.id,
"title": notif.title,
"message": notif.message,
"created_at": notif.created_at.isoformat(),
"read": notif.read
}
# --- Rutas de la pestaña "Cuenta" (Account & Administradores) ---
@router.get("/me")
def get_me(current_admin: AdminUser = Depends(get_current_admin)):
return {
"id": current_admin.id,
"username": current_admin.username,
"role": "admin",
"avatar_url": current_admin.avatar_url,
"perm_users": current_admin.perm_users,
"perm_workers": current_admin.perm_workers,
"perm_churches": current_admin.perm_churches,
"perm_groups": current_admin.perm_groups,
"perm_notifications": current_admin.perm_notifications
}
@router.get("/admins")
def list_admins(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
admins = db.query(AdminUser).order_by(AdminUser.username).all()
return [
{
"id": a.id,
"username": a.username,
"avatar_url": a.avatar_url,
"perm_users": a.perm_users,
"perm_workers": a.perm_workers,
"perm_churches": a.perm_churches,
"perm_groups": a.perm_groups,
"perm_notifications": a.perm_notifications,
"created_at": a.created_at.isoformat() if a.created_at else None
}
for a in admins
]
@router.post("/admins")
def create_admin(data: AdminCreate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
existing = db.query(AdminUser).filter(AdminUser.username == data.username).first()
if existing:
raise HTTPException(status_code=400, detail="El nombre de usuario ya está registrado")
hashed = get_password_hash(data.password)
new_admin = AdminUser(
username=data.username,
hashed_password=hashed,
perm_users=data.perm_users,
perm_workers=data.perm_workers,
perm_churches=data.perm_churches,
perm_groups=data.perm_groups,
perm_notifications=data.perm_notifications
)
db.add(new_admin)
db.commit()
db.refresh(new_admin)
return {
"id": new_admin.id,
"username": new_admin.username,
"avatar_url": new_admin.avatar_url
}
@router.put("/admins/{admin_id}/password")
def update_admin_password(admin_id: int, data: AdminPasswordUpdate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
admin = db.query(AdminUser).filter(AdminUser.id == admin_id).first()
if not admin:
raise HTTPException(status_code=404, detail="Administrador no encontrado")
admin.hashed_password = get_password_hash(data.password)
db.commit()
return {"message": "Contraseña actualizada exitosamente"}
@router.put("/admins/{admin_id}/permissions")
def update_admin_permissions(admin_id: int, data: AdminPermissionsUpdate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
admin = db.query(AdminUser).filter(AdminUser.id == admin_id).first()
if not admin:
raise HTTPException(status_code=404, detail="Administrador no encontrado")
admin.perm_users = data.perm_users
admin.perm_workers = data.perm_workers
admin.perm_churches = data.perm_churches
admin.perm_groups = data.perm_groups
admin.perm_notifications = data.perm_notifications
db.commit()
return {"message": "Permisos actualizados exitosamente"}
@router.put("/admins/{admin_id}/avatar")
def update_admin_avatar(admin_id: int, data: AdminAvatarUpdate, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
admin = db.query(AdminUser).filter(AdminUser.id == admin_id).first()
if not admin:
raise HTTPException(status_code=404, detail="Administrador no encontrado")
admin.avatar_url = data.avatar_url
db.commit()
return {"avatar_url": admin.avatar_url}
@router.delete("/users/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
if not current_admin.perm_users:
raise HTTPException(status_code=403, detail="No tiene permisos para gestionar usuarios")
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
try:
from sqlalchemy import inspect, text
inspector = inspect(db.bind)
db_tables = set(inspector.get_table_names())
# 1. Borrar de tablas secundarias (Cascading manually to prevent FK errors)
deletes = [
("devices", "user_id"),
("credit_transactions", "user_id"),
("tasks", "user_id"),
("partituras_midi", "user_id"),
("user_groups", "user_id"),
("iglesia_members", "user_id"),
("dismissed_visitas", "user_id"),
("progresos_plan", "user_id"),
("puntuaciones_liga", "user_id"),
("participantes_torneo", "user_id")
]
for table, col in deletes:
if table in db_tables:
columns = {c["name"] for c in inspector.get_columns(table)}
if col in columns:
db.execute(text(f"DELETE FROM {table} WHERE {col} = :id"), {"id": user_id})
# 2. Desvincular de tablas (poner en NULL para evitar FK errors)
updates = [
("workers", "user_id"),
("lyrics", "created_by"),
("repertorios", "created_by"),
("iglesias", "admin_id"),
("miembros", "user_id"),
("calendarios", "created_by"),
("asistencias", "registrado_por"),
("finanzas", "registrado_por"),
("momentos", "registrado_por"),
("inventario", "registrado_por"),
("torneos", "creador_id"),
("torneos", "ganador_id")
]
for table, col in updates:
if table in db_tables:
columns = {c["name"] for c in inspector.get_columns(table)}
if col in columns:
db.execute(text(f"UPDATE {table} SET {col} = NULL WHERE {col} = :id"), {"id": user_id})
# 3. Eliminar el usuario principal
db.delete(user)
db.commit()
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"Error al eliminar usuario: {str(e)}")
return {"message": "Usuario eliminado exitosamente"}
# --- Rutas de la pestaña "Contenido" (Landing Page Features) ---
class LandingFeatureUpdate(BaseModel):
icon_svg: Optional[str] = None
icon_color: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
order: Optional[int] = None
class LandingFeatureCreate(BaseModel):
icon_svg: Optional[str] = None
icon_color: str = "cyan"
title: str
description: str
order: int = 0
DEFAULT_FEATURES = [
{
"icon_svg": '',
"icon_color": "cyan",
"title": "Separación de Stems con IA",
"description": "Divide cualquier audio en múltiples pistas independientes: voz, batería, bajo, piano y otros instrumentos con una claridad asombrosa.",
"order": 0,
},
{
"icon_svg": '',
"icon_color": "purple",
"title": "Letras Integradas",
"description": "Agrega y edita fácilmente las letras y cifrados de tus canciones. Visualízalas sincronizadas con tus pistas de acompañamiento.",
"order": 1,
},
{
"icon_svg": '',
"icon_color": "green",
"title": "Transposición Inteligente",
"description": "Modifica el tono de la canción para adaptarla a tu registro vocal, o disminuye la velocidad (tempo) para practicar secciones complejas sin perder nitidez.",
"order": 2,
},
{
"icon_svg": '',
"icon_color": "blue",
"title": "Sincronización Multi-dispositivo",
"description": "Sincroniza tus pistas directamente entre tus dispositivos usando P2P o nube temporal para migrar sin perder nada.",
"order": 3,
},
]
def _seed_features_if_empty(db: Session):
"""Auto-seeds the landing_features table with defaults if it is empty."""
count = db.query(LandingFeature).count()
if count == 0:
for f in DEFAULT_FEATURES:
db.add(LandingFeature(**f))
db.commit()
@router.get("/features")
def list_features(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
_seed_features_if_empty(db)
features = db.query(LandingFeature).order_by(LandingFeature.order).all()
return [
{
"id": f.id,
"icon_svg": f.icon_svg,
"icon_color": f.icon_color,
"title": f.title,
"description": f.description,
"order": f.order,
}
for f in features
]
@router.get("/features/public")
def list_features_public(db: Session = Depends(get_db)):
"""Public endpoint — no auth required — used by the landing page."""
_seed_features_if_empty(db)
features = db.query(LandingFeature).order_by(LandingFeature.order).all()
return [
{
"id": f.id,
"icon_svg": f.icon_svg,
"icon_color": f.icon_color,
"title": f.title,
"description": f.description,
"order": f.order,
}
for f in features
]
@router.put("/features/{feature_id}")
def update_feature(
feature_id: int,
data: LandingFeatureUpdate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
feature = db.query(LandingFeature).filter(LandingFeature.id == feature_id).first()
if not feature:
raise HTTPException(status_code=404, detail="Característica no encontrada")
if data.icon_svg is not None:
feature.icon_svg = data.icon_svg
if data.icon_color is not None:
feature.icon_color = data.icon_color
if data.title is not None:
feature.title = data.title
if data.description is not None:
feature.description = data.description
if data.order is not None:
feature.order = data.order
db.commit()
db.refresh(feature)
return {
"id": feature.id,
"icon_svg": feature.icon_svg,
"icon_color": feature.icon_color,
"title": feature.title,
"description": feature.description,
"order": feature.order,
}
@router.post("/features")
def create_feature(
data: LandingFeatureCreate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
feature = LandingFeature(
icon_svg=data.icon_svg,
icon_color=data.icon_color,
title=data.title,
description=data.description,
order=data.order,
)
db.add(feature)
db.commit()
db.refresh(feature)
return {
"id": feature.id,
"icon_svg": feature.icon_svg,
"icon_color": feature.icon_color,
"title": feature.title,
"description": feature.description,
"order": feature.order,
}
@router.delete("/features/{feature_id}")
def delete_feature(
feature_id: int,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
feature = db.query(LandingFeature).filter(LandingFeature.id == feature_id).first()
if not feature:
raise HTTPException(status_code=404, detail="Característica no encontrada")
db.delete(feature)
db.commit()
return {"message": "Característica eliminada exitosamente"}
# --- Rutas de la pestaña "Contenido" (Landing Page Platforms) ---
class LandingPlatformUpdate(BaseModel):
name: Optional[str] = None
version: Optional[str] = None
os_details: Optional[str] = None
description: Optional[str] = None
download_url: Optional[str] = None
button_text: Optional[str] = None
os_type: Optional[str] = None # windows | android | apple
is_visible: Optional[bool] = None
order: Optional[int] = None
class LandingPlatformCreate(BaseModel):
name: str
version: Optional[str] = None
os_details: Optional[str] = None
description: Optional[str] = None
download_url: Optional[str] = None
button_text: Optional[str] = None
os_type: str = "windows"
is_visible: bool = True
order: int = 0
DEFAULT_PLATFORMS = [
{
"name": "Windows Desktop",
"version": "1.0.4",
"os_details": "Windows 10/11 x64",
"description": "Optimizado para computadoras personales con soporte para procesamiento local fuera de línea.",
"download_url": "#",
"button_text": "Descargar para Windows",
"os_type": "windows",
"is_visible": True,
"order": 0
},
{
"name": "Google Play Store",
"version": "1.2.0",
"os_details": "Android 8.0+",
"description": "Lleva tus ensayos a cualquier parte. Reproduce tus stems y visualiza los acordes en tu tablet o móvil.",
"download_url": "#",
"button_text": "Obtener en Google Play",
"os_type": "android",
"is_visible": True,
"order": 1
},
{
"name": "Apple App Store",
"version": "1.2.0",
"os_details": "iOS / macOS",
"description": "Perfectamente integrado con iPadOS para ver las letras y pistas en tus atriles digitales durante el servicio.",
"download_url": "#",
"button_text": "Descargar para App Store",
"os_type": "apple",
"is_visible": True,
"order": 2
}
]
def _seed_platforms_if_empty(db: Session):
"""Auto-seeds the landing_platforms table with defaults if it is empty."""
count = db.query(LandingPlatform).count()
if count == 0:
for p in DEFAULT_PLATFORMS:
db.add(LandingPlatform(**p))
db.commit()
@router.get("/platforms")
def list_platforms(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
_seed_platforms_if_empty(db)
platforms = db.query(LandingPlatform).order_by(LandingPlatform.order).all()
return [
{
"id": p.id,
"name": p.name,
"version": p.version,
"os_details": p.os_details,
"description": p.description,
"download_url": p.download_url,
"button_text": p.button_text,
"os_type": p.os_type,
"is_visible": p.is_visible,
"order": p.order,
}
for p in platforms
]
@router.get("/platforms/public")
def list_platforms_public(db: Session = Depends(get_db)):
"""Public endpoint — no auth required — used by the landing page."""
_seed_platforms_if_empty(db)
# Only return visible platforms
platforms = db.query(LandingPlatform).filter(LandingPlatform.is_visible == True).order_by(LandingPlatform.order).all()
return [
{
"id": p.id,
"name": p.name,
"version": p.version,
"os_details": p.os_details,
"description": p.description,
"download_url": p.download_url,
"button_text": p.button_text,
"os_type": p.os_type,
"order": p.order,
}
for p in platforms
]
@router.post("/platforms")
def create_platform(
data: LandingPlatformCreate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
platform = LandingPlatform(
name=data.name,
version=data.version,
os_details=data.os_details,
description=data.description,
download_url=data.download_url,
button_text=data.button_text,
os_type=data.os_type,
is_visible=data.is_visible,
order=data.order,
)
db.add(platform)
db.commit()
db.refresh(platform)
return {
"id": platform.id,
"name": platform.name,
"version": platform.version,
"os_details": platform.os_details,
"description": platform.description,
"download_url": platform.download_url,
"button_text": platform.button_text,
"os_type": platform.os_type,
"is_visible": platform.is_visible,
"order": platform.order,
}
@router.put("/platforms/{platform_id}")
def update_platform(
platform_id: int,
data: LandingPlatformUpdate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
platform = db.query(LandingPlatform).filter(LandingPlatform.id == platform_id).first()
if not platform:
raise HTTPException(status_code=404, detail="Plataforma no encontrada")
if data.name is not None:
platform.name = data.name
if data.version is not None:
platform.version = data.version
if data.os_details is not None:
platform.os_details = data.os_details
if data.description is not None:
platform.description = data.description
if data.download_url is not None:
platform.download_url = data.download_url
if data.button_text is not None:
platform.button_text = data.button_text
if data.os_type is not None:
platform.os_type = data.os_type
if data.is_visible is not None:
platform.is_visible = data.is_visible
if data.order is not None:
platform.order = data.order
db.commit()
db.refresh(platform)
return {
"id": platform.id,
"name": platform.name,
"version": platform.version,
"os_details": platform.os_details,
"description": platform.description,
"download_url": platform.download_url,
"button_text": platform.button_text,
"os_type": platform.os_type,
"is_visible": platform.is_visible,
"order": platform.order,
}
@router.delete("/platforms/{platform_id}")
def delete_platform(
platform_id: int,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
platform = db.query(LandingPlatform).filter(LandingPlatform.id == platform_id).first()
if not platform:
raise HTTPException(status_code=404, detail="Plataforma no encontrada")
db.delete(platform)
db.commit()
return {"message": "Plataforma eliminada exitosamente"}
class LandingTutorialCreate(BaseModel):
title: str
desc: Optional[str] = None
steps: List[str] = []
order: int = 0
class LandingTutorialUpdate(BaseModel):
title: Optional[str] = None
desc: Optional[str] = None
steps: Optional[List[str]] = None
order: Optional[int] = None
DEFAULT_TUTORIALS = [
{
"title": "Cómo separar voz e instrumentos",
"desc": "Guía paso a paso para procesar canciones y extraer pistas aisladas (vocales, batería, bajo, etc.) en segundos.",
"steps": [
"Abre la aplicación móvil o de escritorio de Melodix.",
"Dirígete a la pestaña \"Pistas\" y haz clic en el botón de añadir (+).",
"Selecciona el archivo de audio desde tu dispositivo o ingresa un enlace compatible.",
"Elige el modo de separación deseado (por ejemplo: Vocales + Instrumentos, o Stems completos).",
"Confirma y espera a que nuestros servidores de IA procesen tu pista. Esto consumirá 1 crédito.",
"¡Listo! Ya puedes descargar o reproducir cada pista de forma independiente, ajustar volúmenes o mutear instrumentos."
],
"order": 0
},
{
"title": "Ajuste de Tempo y Transposición",
"desc": "Aprende a modificar la velocidad de reproducción y cambiar la clave musical de tus pistas para adaptarlas a tu voz.",
"steps": [
"Selecciona cualquier pista de tu repertorio y abre el reproductor integrado.",
"Para cambiar la clave de tono (transposición), utiliza los botones + y - al lado de \"Tono\". Esto adaptará la pista a tu rango vocal.",
"Para modificar la velocidad, desliza el control de \"Tempo\" o selecciona los valores predeterminados (e.g., 0.75x, 1.25x) para ensayar pasajes difíciles.",
"Los algoritmos de alta fidelidad de Melodix preservarán la calidad del sonido sin distorsiones molestas."
],
"order": 1
},
{
"title": "Migración y Backup Multi-dispositivo",
"desc": "Cambia de teléfono o sincroniza tus dispositivos sin perder nada. (Disponible de forma automática en cuentas premium)",
"steps": [
"Tus pistas procesadas se almacenan temporalmente para que otros dispositivos las descarguen.",
"Sincronización P2P o nube dependiendo de tu plan.",
"Ahorra espacio y mantén tus repertorios idénticos en tus celulares o tablets."
],
"order": 2
}
]
def _seed_tutorials_if_empty(db: Session):
"""Auto-seeds the landing_tutorials table with defaults if it is empty."""
count = db.query(LandingTutorial).count()
if count == 0:
for t in DEFAULT_TUTORIALS:
db.add(LandingTutorial(**t))
db.commit()
@router.get("/tutorials")
def list_tutorials(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
_seed_tutorials_if_empty(db)
tutorials = db.query(LandingTutorial).order_by(LandingTutorial.order).all()
return [
{
"id": t.id,
"title": t.title,
"desc": t.desc,
"steps": t.steps,
"order": t.order,
}
for t in tutorials
]
@router.get("/tutorials/public")
def list_tutorials_public(db: Session = Depends(get_db)):
"""Public endpoint — no auth required — used by the landing page."""
_seed_tutorials_if_empty(db)
tutorials = db.query(LandingTutorial).order_by(LandingTutorial.order).all()
return [
{
"id": t.id,
"title": t.title,
"desc": t.desc,
"steps": t.steps,
"order": t.order,
}
for t in tutorials
]
@router.post("/tutorials")
def create_tutorial(
data: LandingTutorialCreate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
tutorial = LandingTutorial(
title=data.title,
desc=data.desc,
steps=data.steps,
order=data.order,
)
db.add(tutorial)
db.commit()
db.refresh(tutorial)
return {
"id": tutorial.id,
"title": tutorial.title,
"desc": tutorial.desc,
"steps": tutorial.steps,
"order": tutorial.order,
}
@router.put("/tutorials/{tutorial_id}")
def update_tutorial(
tutorial_id: int,
data: LandingTutorialUpdate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
tutorial = db.query(LandingTutorial).filter(LandingTutorial.id == tutorial_id).first()
if not tutorial:
raise HTTPException(status_code=404, detail="Tutorial no encontrado")
if data.title is not None:
tutorial.title = data.title
if data.desc is not None:
tutorial.desc = data.desc
if data.steps is not None:
tutorial.steps = data.steps
if data.order is not None:
tutorial.order = data.order
db.commit()
db.refresh(tutorial)
return {
"id": tutorial.id,
"title": tutorial.title,
"desc": tutorial.desc,
"steps": tutorial.steps,
"order": tutorial.order,
}
@router.delete("/tutorials/{tutorial_id}")
def delete_tutorial(
tutorial_id: int,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
tutorial = db.query(LandingTutorial).filter(LandingTutorial.id == tutorial_id).first()
if not tutorial:
raise HTTPException(status_code=404, detail="Tutorial no encontrado")
db.delete(tutorial)
db.commit()
return {"message": "Tutorial de landing page eliminado exitosamente"}
class LandingGalleryItemCreate(BaseModel):
image_url: str
caption: Optional[str] = None
order: int = 0
class LandingGalleryItemUpdate(BaseModel):
image_url: Optional[str] = None
caption: Optional[str] = None
order: Optional[int] = None
DEFAULT_GALLERY = [
{
"image_url": "/hero_banner.png",
"caption": "Plan de Estudios & Desafíos",
"order": 0
},
{
"image_url": "/app_mockup.png",
"caption": "Liga de la Iglesia, Perfil & Finanzas",
"order": 1
}
]
def _seed_gallery_if_empty(db: Session):
"""Auto-seeds the landing_gallery table with defaults if empty."""
count = db.query(LandingGalleryItem).count()
if count == 0:
for item in DEFAULT_GALLERY:
db.add(LandingGalleryItem(**item))
db.commit()
@router.get("/gallery")
def list_gallery(db: Session = Depends(get_db), current_admin: AdminUser = Depends(get_current_admin)):
_seed_gallery_if_empty(db)
items = db.query(LandingGalleryItem).order_by(LandingGalleryItem.order).all()
return [
{
"id": i.id,
"image_url": i.image_url,
"caption": i.caption,
"order": i.order,
}
for i in items
]
@router.get("/gallery/public")
def list_gallery_public(db: Session = Depends(get_db)):
"""Public endpoint for the landing page gallery."""
_seed_gallery_if_empty(db)
items = db.query(LandingGalleryItem).order_by(LandingGalleryItem.order).all()
return [
{
"id": i.id,
"image_url": i.image_url,
"caption": i.caption,
"order": i.order,
}
for i in items
]
@router.post("/gallery")
def create_gallery_item(
data: LandingGalleryItemCreate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
item = LandingGalleryItem(
image_url=data.image_url,
caption=data.caption,
order=data.order,
)
db.add(item)
db.commit()
db.refresh(item)
return {
"id": item.id,
"image_url": item.image_url,
"caption": item.caption,
"order": item.order,
}
@router.put("/gallery/{item_id}")
def update_gallery_item(
item_id: int,
data: LandingGalleryItemUpdate,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
item = db.query(LandingGalleryItem).filter(LandingGalleryItem.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="Item de galería no encontrado")
if data.image_url is not None:
item.image_url = data.image_url
if data.caption is not None:
item.caption = data.caption
if data.order is not None:
item.order = data.order
db.commit()
db.refresh(item)
return {
"id": item.id,
"image_url": item.image_url,
"caption": item.caption,
"order": item.order,
}
@router.delete("/gallery/{item_id}")
def delete_gallery_item(
item_id: int,
db: Session = Depends(get_db),
current_admin: AdminUser = Depends(get_current_admin),
):
item = db.query(LandingGalleryItem).filter(LandingGalleryItem.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="Item de galería no encontrado")
db.delete(item)
db.commit()
return {"message": "Item de galería eliminado exitosamente"}