Migrate TickTick OAuth and action tasks

This commit is contained in:
2026-04-20 17:06:03 +02:00
parent 179aae264e
commit 982af62f4f
17 changed files with 1114 additions and 15 deletions
+4 -1
View File
@@ -18,7 +18,7 @@ from app.services.auth import (
revoke_session,
validate_csrf_token,
)
from app.services.config_page import build_config_sections
from app.services.config_page import build_config_sections, is_ticktick_oauth_ready
logger = logging.getLogger(__name__)
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parents[2] / "templates"))
@@ -225,6 +225,9 @@ def _render_config_page(
"config_error": None,
"config_saved": False,
"config_sections": build_config_sections(auth_db_session, settings),
"ticktick_oauth_ready": is_ticktick_oauth_ready(settings),
"ticktick_oauth_notice": None,
"ticktick_oauth_error": None,
},
status_code=status_code,
)
+12 -3
View File
@@ -6,7 +6,8 @@ from fastapi.responses import PlainTextResponse, Response
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.dependencies import get_db
from app.dependencies import get_db, get_ticktick_client
from app.integrations.ticktick import TickTickClient, TickTickConfigError, TickTickRequestError
from app.schemas.homeassistant import HomeAssistantPublishEnvelope
from app.services.homeassistant_inbound import (
UnsupportedHomeAssistantMessage,
@@ -21,13 +22,15 @@ INTERNAL_SERVER_ERROR_MESSAGE = "internal server error"
@router.post("/homeassistant/publish")
async def publish_from_homeassistant(
request: Request, db: Session = Depends(get_db)
request: Request,
db: Session = Depends(get_db),
ticktick_client: TickTickClient = Depends(get_ticktick_client),
) -> Response:
try:
raw_payload = await request.body()
data = json.loads(raw_payload)
envelope = HomeAssistantPublishEnvelope.model_validate(data)
handle_homeassistant_message(db, envelope)
handle_homeassistant_message(db, envelope, ticktick_client)
except json.JSONDecodeError as exc:
logger.warning("Rejected Home Assistant publish request due to invalid JSON: %s", exc)
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
@@ -42,6 +45,12 @@ async def publish_from_homeassistant(
INTERNAL_SERVER_ERROR_MESSAGE,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
except (TickTickConfigError, TickTickRequestError, RuntimeError) as exc:
logger.warning("Home Assistant publish request failed during TickTick handling: %s", exc)
return PlainTextResponse(
INTERNAL_SERVER_ERROR_MESSAGE,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
except ValueError as exc:
logger.warning("Rejected Home Assistant publish request due to invalid content: %s", exc)
return PlainTextResponse(BAD_REQUEST_MESSAGE, status_code=status.HTTP_400_BAD_REQUEST)
+32 -2
View File
@@ -8,7 +8,12 @@ from fastapi.templating import Jinja2Templates
from app.config import Settings, get_settings
from app.dependencies import get_app_settings, get_auth_db, get_current_auth_session
from app.services.auth import AuthenticatedSession
from app.services.config_page import ConfigSaveError, build_config_sections, save_config_updates
from app.services.config_page import (
ConfigSaveError,
build_config_sections,
is_ticktick_oauth_ready,
save_config_updates,
)
from sqlalchemy.orm import Session
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parents[2] / "templates"))
@@ -16,6 +21,18 @@ router = APIRouter(tags=["pages"])
logger = logging.getLogger(__name__)
def _ticktick_oauth_notice(status_value: str | None) -> tuple[str | None, str | None]:
if status_value == "success":
return "TickTick authorization completed successfully.", None
if status_value == "invalid-state":
return None, "TickTick authorization failed due to invalid OAuth state. Start the flow again."
if status_value == "invalid-callback":
return None, "TickTick authorization callback was missing required parameters."
if status_value == "failed":
return None, "TickTick authorization failed. Check server logs for the provider response and verify TickTick app credentials and redirect URI."
return None, None
@router.get("/", response_class=HTMLResponse)
def home(
request: Request,
@@ -46,6 +63,10 @@ def config_page(
if current_auth is None:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
ticktick_oauth_notice, ticktick_oauth_error = _ticktick_oauth_notice(
request.query_params.get("ticktick_oauth")
)
context = {
"app_name": settings.app_name,
"app_env": settings.app_env,
@@ -56,6 +77,9 @@ def config_page(
"config_error": None,
"config_saved": request.query_params.get("saved") == "1",
"config_sections": build_config_sections(auth_db_session, settings),
"ticktick_oauth_ready": is_ticktick_oauth_ready(settings),
"ticktick_oauth_notice": ticktick_oauth_notice,
"ticktick_oauth_error": ticktick_oauth_error,
}
return templates.TemplateResponse(request, "config.html", context)
@@ -84,6 +108,9 @@ async def config_submit(
"config_error": "invalid config update request",
"config_saved": False,
"config_sections": build_config_sections(auth_db_session, settings),
"ticktick_oauth_ready": is_ticktick_oauth_ready(settings),
"ticktick_oauth_notice": None,
"ticktick_oauth_error": None,
}
return templates.TemplateResponse(
request,
@@ -96,7 +123,7 @@ async def config_submit(
save_config_updates(auth_db_session, dict(form), settings)
except ConfigSaveError:
logger.warning("Rejected config update due to invalid submitted values")
refreshed_settings = build_runtime_settings(auth_db_session, get_settings())
refreshed_settings = get_settings()
context = {
"app_name": refreshed_settings.app_name,
"app_env": refreshed_settings.app_env,
@@ -107,6 +134,9 @@ async def config_submit(
"config_error": "invalid config submission",
"config_saved": False,
"config_sections": build_config_sections(auth_db_session, refreshed_settings),
"ticktick_oauth_ready": is_ticktick_oauth_ready(refreshed_settings),
"ticktick_oauth_notice": None,
"ticktick_oauth_error": None,
}
return templates.TemplateResponse(
request,
+79
View File
@@ -0,0 +1,79 @@
import logging
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import PlainTextResponse, RedirectResponse, Response
from sqlalchemy.orm import Session
from app.config import Settings
from app.dependencies import (
get_app_settings,
get_auth_db,
get_current_auth_session,
get_ticktick_client,
)
from app.integrations.ticktick import TickTickAuthError, TickTickClient, TickTickConfigError, TickTickRequestError
from app.services.auth import AuthenticatedSession
from app.services.config_page import save_config_value
router = APIRouter(tags=["ticktick"])
logger = logging.getLogger(__name__)
@router.get("/ticktick/auth/start")
def start_ticktick_auth(
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
ticktick_client: TickTickClient = Depends(get_ticktick_client),
) -> Response:
if current_auth is None:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
try:
authorization_url = ticktick_client.build_authorization_url()
except TickTickConfigError as exc:
logger.warning("Rejected TickTick OAuth start due to incomplete configuration: %s", exc)
return PlainTextResponse("TickTick integration is not configured", status_code=400)
return RedirectResponse(url=authorization_url, status_code=status.HTTP_303_SEE_OTHER)
@router.get("/ticktick/auth/code")
def handle_ticktick_auth_code(
request: Request,
auth_db_session: Session = Depends(get_auth_db),
settings: Settings = Depends(get_app_settings),
ticktick_client: TickTickClient = Depends(get_ticktick_client),
) -> Response:
code = request.query_params.get("code", "")
state = request.query_params.get("state", "")
if not code or not state:
return RedirectResponse(
url="/config?ticktick_oauth=invalid-callback",
status_code=status.HTTP_303_SEE_OTHER,
)
try:
token = ticktick_client.exchange_authorization_code(code=code, state=state)
save_config_value(
auth_db_session,
env_name="TICKTICK_TOKEN",
value=token,
bootstrap_settings=settings,
)
except TickTickAuthError as exc:
logger.warning("Rejected TickTick OAuth callback due to invalid state: %s", exc)
return RedirectResponse(
url="/config?ticktick_oauth=invalid-state",
status_code=status.HTTP_303_SEE_OTHER,
)
except (TickTickConfigError, TickTickRequestError, ValueError) as exc:
logger.warning("TickTick OAuth callback failed: %s", exc)
return RedirectResponse(
url="/config?ticktick_oauth=failed",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse(
url="/config?ticktick_oauth=success",
status_code=status.HTTP_303_SEE_OTHER,
)