melodix-api / app /routers /admin.py
GitHub Action
deploy from github actions
ec7fd73
Raw
History Blame Contribute Delete
39 kB
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": '<path d="M4 10h16M4 14h16M12 4v16"/>',
"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": '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>',
"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": '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
"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": '<path d="M18 10.286A6.5 6.5 0 0 0 5.828 7.32a4 4 0 1 0-.22 7.848H18a3.42 3.42 0 0 0 0-6.882z"/>',
"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"}