feat(tempo): per-user Tempo API token — same pattern as Jira token
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Each user can now store their own personal Tempo API token in their profile settings. Time is logged using each user's own credentials. Backend: - Migration b044: adds tempo_api_token column to users table - User model: adds tempo_api_token column - UserPreferencesUpdate: adds tempo_api_token field (write-only) - UserOut: adds tempo_api_token (excluded) + tempo_token_set bool; @model_validator derives both jira_token_set and tempo_token_set - users router: handles tempo_api_token same as jira_api_token (empty string clears it, never returned in responses) - tempo_service: refactored to per-user token; has_tempo_configured(), get_user_tempo_client(user) use user.tempo_api_token; global TEMPO_ENABLED still acts as kill-switch - system router: /system/tempo-test now uses current user's personal token (any role); removed global TEMPO_API_TOKEN dependency Frontend: - settings.ts: UserPreferencesUpdate.tempo_api_token, UserMeOut.tempo_token_set - SettingsPage ProfileSection: Tempo Integration section with password field, show/hide toggle, configured badge, and Test Tempo button — mirrors the Jira token UX exactly - JiraConfigSection: removed stale global Tempo test block Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import SessionLocal, get_db
|
||||
from app.dependencies.auth import require_role
|
||||
from app.dependencies.auth import get_current_user, require_role
|
||||
from app.models.user import User
|
||||
from app.services.mitre_sync_service import sync_mitre
|
||||
from app.services.intel_service import scan_intel
|
||||
@@ -357,25 +357,24 @@ def test_jira_connection(
|
||||
@router.post("/tempo-test")
|
||||
def test_tempo_connection(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Test the Tempo connection and report configuration status.
|
||||
"""Test the current user's personal Tempo connection.
|
||||
|
||||
Uses the Tempo API token stored in the user's profile (not a global token).
|
||||
Always returns HTTP 200 with a ``status`` field so Cloudflare never
|
||||
intercepts the response.
|
||||
"""
|
||||
from app.config import settings
|
||||
from app.services.tempo_service import has_tempo_configured
|
||||
|
||||
if not settings.TEMPO_ENABLED:
|
||||
return {
|
||||
"status": "disabled",
|
||||
"message": "Tempo is not enabled. Set TEMPO_ENABLED=true and TEMPO_API_TOKEN in your environment.",
|
||||
}
|
||||
|
||||
if not settings.TEMPO_API_TOKEN:
|
||||
tempo_token = getattr(current_user, "tempo_api_token", None)
|
||||
if not tempo_token:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "TEMPO_API_TOKEN is empty. Add it to your environment.",
|
||||
"message": (
|
||||
"No Tempo API token configured. "
|
||||
"Add it in Settings → Profile → Tempo Integration."
|
||||
),
|
||||
}
|
||||
|
||||
jira_account_id = getattr(current_user, "jira_account_id", None)
|
||||
@@ -383,15 +382,15 @@ def test_tempo_connection(
|
||||
return {
|
||||
"status": "error",
|
||||
"message": (
|
||||
"Your user has no Jira Account ID configured. "
|
||||
"No Jira Account ID configured. "
|
||||
"Set it in Settings → Profile → Jira Integration → Account ID."
|
||||
),
|
||||
}
|
||||
|
||||
try:
|
||||
from tempoapiclient import client_v4 as tempo_client
|
||||
tempo = tempo_client.Tempo(auth_token=settings.TEMPO_API_TOKEN)
|
||||
# Fetch current user's worklogs as a connectivity check (limit 1)
|
||||
tempo = tempo_client.Tempo(auth_token=tempo_token)
|
||||
# Use a minimal date range to verify connectivity without fetching much data
|
||||
worklogs = tempo.get_worklogs_by_account_id(
|
||||
account_id=jira_account_id,
|
||||
dateFrom="2024-01-01",
|
||||
@@ -405,12 +404,12 @@ def test_tempo_connection(
|
||||
except Exception as exc:
|
||||
err = str(exc)
|
||||
if "401" in err or "Unauthorized" in err:
|
||||
msg = "Authentication failed (401). Check your TEMPO_API_TOKEN."
|
||||
msg = "Authentication failed (401). Check your Tempo API token."
|
||||
elif "403" in err or "Forbidden" in err:
|
||||
msg = "Access denied (403). The Tempo token may not have the required permissions."
|
||||
msg = "Access denied (403). The token may not have the required Tempo permissions."
|
||||
else:
|
||||
msg = f"Tempo connection failed: {err}"
|
||||
logger.warning("Tempo test connection failed: %s", err)
|
||||
logger.warning("Tempo test connection failed for user %s: %s", current_user.username, err)
|
||||
return {"status": "error", "message": msg}
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def update_my_preferences(
|
||||
"""
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
if field in ("jira_api_token", "jira_email"):
|
||||
if field in ("jira_api_token", "jira_email", "tempo_api_token"):
|
||||
# Empty string means "clear the value"
|
||||
setattr(current_user, field, value if value else None)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user