diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index 5743319..72cfa86 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -295,7 +295,8 @@ def test_jira_connection( try: jira = get_user_jira_client(current_user, db) - # Lightweight call: get current user info + # Lightweight call: get current user info (10 s hard timeout) + jira._session.timeout = 10 # type: ignore[attr-defined] myself = jira.myself() return { "status": "ok", @@ -303,10 +304,22 @@ def test_jira_connection( "jira_url": jira_url, } except Exception as exc: - raise HTTPException( - status_code=502, - detail=f"Jira connection failed: {exc}", - ) + err = str(exc) + # Translate common Atlassian-library errors into human-readable messages + if "Expecting value" in err or "line 1 column 1" in err: + detail = ( + "Jira returned an unexpected response — check that the URL is correct " + "and that the account email + API token are valid." + ) + elif "401" in err or "Unauthorized" in err: + detail = "Authentication failed (401). Verify your Atlassian email and API token." + elif "403" in err or "Forbidden" in err: + detail = "Access denied (403). The token may not have permission to access this Jira instance." + elif "timed out" in err.lower() or "timeout" in err.lower(): + detail = "Connection timed out. Check the Jira URL is reachable from the server." + else: + detail = f"Jira connection failed: {err}" + raise HTTPException(status_code=502, detail=detail) # --------------------------------------------------------------------------- diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index ecb3fd6..6cb6ebe 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -4,7 +4,7 @@ import re import uuid from datetime import datetime -from pydantic import BaseModel, ConfigDict, EmailStr, field_validator +from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator # ── Username policy ───────────────────────────────────────────────── @@ -148,15 +148,20 @@ class UserOut(BaseModel): notification_preferences: dict | None = None jira_account_id: str | None = None jira_email: str | None = None - # Never return the raw token — just indicate whether it is configured. + # Read from ORM but NEVER exposed in responses — used only to derive jira_token_set. + jira_api_token: str | None = Field(default=None, exclude=True) + # True when the user has a personal Atlassian token stored. jira_token_set: bool = False model_config = ConfigDict(from_attributes=True) - @classmethod - def model_validate(cls, obj, *args, **kwargs): # type: ignore[override] - instance = super().model_validate(obj, *args, **kwargs) - # Derive jira_token_set from the ORM object without exposing the value - if hasattr(obj, "jira_api_token"): - instance.jira_token_set = bool(obj.jira_api_token) - return instance + @model_validator(mode="after") + def _derive_jira_token_set(self) -> "UserOut": + """Set jira_token_set from the (excluded) jira_api_token field. + + Uses @model_validator(mode='after') so Pydantic's Rust core calls it + during FastAPI response serialisation — model_validate() overrides are + bypassed by FastAPI's __pydantic_validator__.validate_python() path. + """ + self.jira_token_set = bool(self.jira_api_token) + return self