import logging from pathlib import Path from fastapi import APIRouter, Depends, Request, status from fastapi.responses import HTMLResponse, RedirectResponse, Response 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, is_ticktick_oauth_ready, save_config_updates, ) from app.services.email import EmailConfigurationError, EmailDeliveryError, is_smtp_ready, send_smtp_test_email from sqlalchemy.orm import Session templates = Jinja2Templates(directory=str(Path(__file__).resolve().parents[2] / "templates")) 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 def _smtp_test_notice(status_value: str | None) -> tuple[str | None, str | None]: if status_value == "success": return "SMTP test email sent successfully.", None if status_value == "config-error": return None, "SMTP test failed. Check required SMTP settings before sending a test email." if status_value == "failed": return None, "SMTP test failed. Check saved SMTP settings and server reachability." return None, None def _build_config_context( *, auth_db_session: Session, settings: Settings, current_auth: AuthenticatedSession, config_saved: bool, config_error: str | None, password_change_error: str | None, ticktick_oauth_notice: str | None, ticktick_oauth_error: str | None, smtp_test_notice: str | None, smtp_test_error: str | None, ) -> dict[str, object]: return { "app_name": settings.app_name, "app_env": settings.app_env, "current_username": current_auth.user.username, "csrf_token": current_auth.session.csrf_token, "force_password_change": current_auth.user.force_password_change, "password_change_error": password_change_error, "config_error": config_error, "config_saved": config_saved, "config_sections": build_config_sections(auth_db_session, settings), "ticktick_oauth_ready": is_ticktick_oauth_ready(settings), "ticktick_redirect_uri": settings.ticktick_redirect_uri, "ticktick_oauth_notice": ticktick_oauth_notice, "ticktick_oauth_error": ticktick_oauth_error, "smtp_test_ready": is_smtp_ready(settings), "smtp_test_notice": smtp_test_notice, "smtp_test_error": smtp_test_error, } @router.get("/", response_class=HTMLResponse) def home( request: Request, current_auth: AuthenticatedSession | None = Depends(get_current_auth_session), ) -> RedirectResponse: if current_auth is None: return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER) return RedirectResponse(url="/config", status_code=status.HTTP_303_SEE_OTHER) @router.get("/admin", response_class=HTMLResponse) def admin_redirect( request: Request, current_auth: AuthenticatedSession | None = Depends(get_current_auth_session), ) -> RedirectResponse: if current_auth is None: return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER) return RedirectResponse(url="/config", status_code=status.HTTP_303_SEE_OTHER) @router.get("/config", response_class=HTMLResponse) def config_page( request: Request, auth_db_session: Session = Depends(get_auth_db), settings: Settings = Depends(get_app_settings), current_auth: AuthenticatedSession | None = Depends(get_current_auth_session), ) -> Response: 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") ) smtp_test_notice, smtp_test_error = _smtp_test_notice(request.query_params.get("smtp_test")) context = _build_config_context( auth_db_session=auth_db_session, settings=settings, current_auth=current_auth, config_saved=request.query_params.get("saved") == "1", config_error=None, password_change_error=None, ticktick_oauth_notice=ticktick_oauth_notice, ticktick_oauth_error=ticktick_oauth_error, smtp_test_notice=smtp_test_notice, smtp_test_error=smtp_test_error, ) return templates.TemplateResponse(request, "config.html", context) @router.post("/config", response_class=HTMLResponse) async def config_submit( request: Request, auth_db_session: Session = Depends(get_auth_db), settings: Settings = Depends(get_app_settings), current_auth: AuthenticatedSession | None = Depends(get_current_auth_session), ) -> Response: if current_auth is None: return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER) form = await request.form() csrf_token = form.get("csrf_token") if csrf_token != current_auth.session.csrf_token: logger.warning("Rejected config update due to CSRF validation failure") context = _build_config_context( auth_db_session=auth_db_session, settings=settings, current_auth=current_auth, config_saved=False, config_error="invalid config update request", password_change_error=None, ticktick_oauth_notice=None, ticktick_oauth_error=None, smtp_test_notice=None, smtp_test_error=None, ) return templates.TemplateResponse( request, "config.html", context, status_code=status.HTTP_400_BAD_REQUEST, ) try: save_config_updates(auth_db_session, dict(form), settings) except ConfigSaveError: logger.warning("Rejected config update due to invalid submitted values") refreshed_settings = get_settings() context = _build_config_context( auth_db_session=auth_db_session, settings=refreshed_settings, current_auth=current_auth, config_saved=False, config_error="invalid config submission", password_change_error=None, ticktick_oauth_notice=None, ticktick_oauth_error=None, smtp_test_notice=None, smtp_test_error=None, ) return templates.TemplateResponse( request, "config.html", context, status_code=status.HTTP_400_BAD_REQUEST, ) return RedirectResponse(url="/config?saved=1", status_code=status.HTTP_303_SEE_OTHER) @router.post("/config/smtp/test", response_class=HTMLResponse) async def smtp_test_submit( request: Request, auth_db_session: Session = Depends(get_auth_db), settings: Settings = Depends(get_app_settings), current_auth: AuthenticatedSession | None = Depends(get_current_auth_session), ) -> Response: if current_auth is None: return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER) form = await request.form() csrf_token = form.get("csrf_token") if csrf_token != current_auth.session.csrf_token: logger.warning("Rejected SMTP test due to CSRF validation failure") context = _build_config_context( auth_db_session=auth_db_session, settings=settings, current_auth=current_auth, config_saved=False, config_error=None, password_change_error=None, ticktick_oauth_notice=None, ticktick_oauth_error=None, smtp_test_notice=None, smtp_test_error="invalid SMTP test request", ) return templates.TemplateResponse( request, "config.html", context, status_code=status.HTTP_400_BAD_REQUEST, ) try: send_smtp_test_email(settings) except EmailConfigurationError as exc: logger.warning("SMTP test email rejected due to configuration: %s", exc) return RedirectResponse( url="/config?smtp_test=config-error", status_code=status.HTTP_303_SEE_OTHER, ) except EmailDeliveryError as exc: logger.warning("SMTP test email failed: %s", exc) return RedirectResponse( url="/config?smtp_test=failed", status_code=status.HTTP_303_SEE_OTHER, ) return RedirectResponse( url="/config?smtp_test=success", status_code=status.HTTP_303_SEE_OTHER, )