Files
weibo_signin/backend/api_service/app/routers/tasks.py

197 lines
5.5 KiB
Python
Raw Normal View History

2026-03-09 14:05:00 +08:00
"""
Signin Task CRUD router.
All endpoints require JWT authentication and enforce resource ownership.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from croniter import croniter
import redis.asyncio as aioredis
import json
from shared.models import get_db, Account, Task, User
from shared.config import shared_settings
from shared.response import success_response
from api_service.app.dependencies import get_current_user
from api_service.app.schemas.task import (
TaskCreate,
TaskUpdate,
TaskResponse,
)
router = APIRouter(prefix="/api/v1", tags=["tasks"])
def _task_to_dict(task: Task) -> dict:
return TaskResponse.model_validate(task).model_dump(mode="json")
async def _get_owned_account(
account_id: str,
user: User,
db: AsyncSession,
) -> Account:
"""Fetch an account and verify it belongs to the current user."""
result = await db.execute(select(Account).where(Account.id == account_id))
account = result.scalar_one_or_none()
if account is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
if account.user_id != user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
return account
async def _get_owned_task(
task_id: str,
user: User,
db: AsyncSession,
) -> Task:
"""Fetch a task and verify it belongs to the current user."""
from sqlalchemy.orm import selectinload
result = await db.execute(
select(Task)
.options(selectinload(Task.account))
.where(Task.id == task_id)
)
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
# Verify ownership through account
if task.account.user_id != user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
return task
def _validate_cron_expression(cron_expr: str) -> None:
"""Validate cron expression format using croniter."""
try:
croniter(cron_expr)
except (ValueError, KeyError) as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid cron expression: {str(e)}"
)
async def _notify_scheduler(action: str, task_data: dict) -> None:
"""Notify Task_Scheduler via Redis pub/sub about task changes."""
try:
redis_client = aioredis.from_url(
shared_settings.REDIS_URL,
encoding="utf-8",
decode_responses=True
)
message = {
"action": action, # "create", "update", "delete"
"task": task_data
}
await redis_client.publish("task_updates", json.dumps(message))
await redis_client.close()
except Exception as e:
# Log but don't fail the request if notification fails
print(f"Warning: Failed to notify scheduler: {e}")
# ---- CREATE TASK ----
@router.post("/accounts/{account_id}/tasks", status_code=status.HTTP_201_CREATED)
async def create_task(
account_id: str,
body: TaskCreate,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new signin task for the specified account."""
# Verify account ownership
account = await _get_owned_account(account_id, user, db)
# Validate cron expression
_validate_cron_expression(body.cron_expression)
# Create task
task = Task(
account_id=account.id,
cron_expression=body.cron_expression,
is_enabled=True,
)
db.add(task)
await db.commit()
await db.refresh(task)
# Notify scheduler
await _notify_scheduler("create", _task_to_dict(task))
return success_response(_task_to_dict(task), "Task created")
# ---- LIST TASKS FOR ACCOUNT ----
@router.get("/accounts/{account_id}/tasks")
async def list_tasks(
account_id: str,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get all tasks for the specified account."""
# Verify account ownership
account = await _get_owned_account(account_id, user, db)
# Fetch tasks
result = await db.execute(
select(Task).where(Task.account_id == account.id)
)
tasks = result.scalars().all()
return success_response(
[_task_to_dict(t) for t in tasks],
"Tasks retrieved",
)
# ---- UPDATE TASK ----
@router.put("/tasks/{task_id}")
async def update_task(
task_id: str,
body: TaskUpdate,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update task (enable/disable)."""
task = await _get_owned_task(task_id, user, db)
if body.is_enabled is not None:
task.is_enabled = body.is_enabled
await db.commit()
await db.refresh(task)
# Notify scheduler
await _notify_scheduler("update", _task_to_dict(task))
return success_response(_task_to_dict(task), "Task updated")
# ---- DELETE TASK ----
@router.delete("/tasks/{task_id}")
async def delete_task(
task_id: str,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Delete a task."""
task = await _get_owned_task(task_id, user, db)
task_data = _task_to_dict(task)
await db.delete(task)
await db.commit()
# Notify scheduler
await _notify_scheduler("delete", task_data)
return success_response(None, "Task deleted")