Migrate TickTick OAuth and action tasks
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user