update auth type

This commit is contained in:
CREDO23 2025-07-24 22:45:47 +02:00
parent 7af65a5a66
commit 4984aab3f1
6 changed files with 245 additions and 140 deletions

View file

@ -5,8 +5,10 @@ A module for retrieving data from Jira.
Allows fetching issue lists and their comments, projects and more.
"""
from typing import Any, Dict, List, Optional, Tuple
import base64
import json
from datetime import datetime
from typing import Any, Dict, List, Optional
import requests
@ -17,55 +19,76 @@ class JiraConnector:
def __init__(
self,
base_url: Optional[str] = None,
personal_access_token: Optional[str] = None,
email: Optional[str] = None,
api_token: Optional[str] = None,
):
"""
Initialize the JiraConnector class.
Args:
base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') (optional)
personal_access_token: Jira personal access token (optional)
email: Jira account email address (optional)
api_token: Jira API token (optional)
"""
self.base_url = base_url.rstrip("/") if base_url else None
self.personal_access_token = personal_access_token
self.email = email
self.api_token = api_token
self.api_version = "3" # Jira Cloud API version
def set_credentials(self, base_url: str, personal_access_token: str) -> None:
def set_credentials(self, base_url: str, email: str, api_token: str) -> None:
"""
Set the Jira credentials.
Args:
base_url: Jira instance base URL
personal_access_token: Jira personal access token
email: Jira account email address
api_token: Jira API token
"""
self.base_url = base_url.rstrip("/")
self.personal_access_token = personal_access_token
self.email = email
self.api_token = api_token
def set_personal_access_token(self, personal_access_token: str) -> None:
def set_email(self, email: str) -> None:
"""
Set the Jira personal access token.
Set the Jira account email.
Args:
personal_access_token: Jira personal access token
email: Jira account email address
"""
self.personal_access_token = personal_access_token
self.email = email
def set_api_token(self, api_token: str) -> None:
"""
Set the Jira API token.
Args:
api_token: Jira API token
"""
self.api_token = api_token
def get_headers(self) -> Dict[str, str]:
"""
Get headers for Jira API requests.
Get headers for Jira API requests using Basic Authentication.
Returns:
Dictionary of headers
Raises:
ValueError: If personal_access_token or base_url have not been set
ValueError: If email, api_token, or base_url have not been set
"""
if not all([self.base_url, self.personal_access_token]):
raise ValueError("Jira personal access token or base URL not initialized.")
if not all([self.base_url, self.email, self.api_token]):
raise ValueError(
"Jira credentials not initialized. Call set_credentials() first."
)
# Create Basic Auth header using email:api_token
auth_str = f"{self.email}:{self.api_token}"
auth_bytes = auth_str.encode("utf-8")
auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii")
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.personal_access_token}",
"Authorization": auth_header,
"Accept": "application/json",
}
@ -83,17 +106,21 @@ class JiraConnector:
Response data from the API
Raises:
ValueError: If personal_access_token or base_url have not been set
ValueError: If email, api_token, or base_url have not been set
Exception: If the API request fails
"""
if not all([self.base_url, self.personal_access_token]):
raise ValueError("Jira personal access token or base URL not initialized.")
if not all([self.base_url, self.email, self.api_token]):
raise ValueError(
"Jira credentials not initialized. Call set_credentials() first."
)
url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}"
headers = self.get_headers()
response = requests.get(url, headers=headers, params=params, timeout=500)
print(json.dumps(response.json(), indent=2))
if response.status_code == 200:
return response.json()
else:
@ -197,9 +224,11 @@ class JiraConnector:
try:
# Build JQL query for date range
# Query issues that were either created OR updated within the date range
date_filter = f"(created >= '{start_date}' AND created <= '{end_date}') OR (updated >= '{start_date}' AND updated <= '{end_date}')"
date_filter = (
f"(createdDate >= '{start_date}' AND createdDate <= '{end_date}')"
)
jql = f"{date_filter} ORDER BY created DESC"
jql = f"{date_filter}"
if project_key:
jql = (
f'project = "{project_key}" AND {date_filter} ORDER BY created DESC'
@ -234,8 +263,11 @@ class JiraConnector:
while True:
params["startAt"] = start_at
print(json.dumps(params, indent=2))
result = self.make_api_request("search", params)
print(json.dumps(result, indent=2))
if not isinstance(result, dict) or "issues" not in result:
return [], "Invalid response from Jira API"

View file

@ -1,104 +1,112 @@
import unittest
from unittest.mock import patch, Mock
from datetime import datetime
from unittest.mock import Mock, patch
# Import the JiraConnector
from .jira_connector import JiraConnector
class TestJiraConnector(unittest.TestCase):
def setUp(self):
"""Set up test fixtures."""
self.base_url = "https://test.atlassian.net"
self.token = "test_token"
self.connector = JiraConnector(base_url=self.base_url, personal_access_token=self.token)
self.email = "test@example.com"
self.api_token = "test_api_token"
self.connector = JiraConnector(
base_url=self.base_url, email=self.email, api_token=self.api_token
)
def test_init(self):
"""Test JiraConnector initialization."""
self.assertEqual(self.connector.base_url, self.base_url)
self.assertEqual(self.connector.personal_access_token, self.token)
self.assertEqual(self.connector.email, self.email)
self.assertEqual(self.connector.api_token, self.api_token)
self.assertEqual(self.connector.api_version, "3")
def test_init_with_trailing_slash(self):
"""Test JiraConnector initialization with trailing slash in URL."""
connector = JiraConnector(base_url="https://test.atlassian.net/", personal_access_token=self.token)
connector = JiraConnector(
base_url="https://test.atlassian.net/",
email=self.email,
api_token=self.api_token,
)
self.assertEqual(connector.base_url, "https://test.atlassian.net")
def test_set_credentials(self):
"""Test setting credentials."""
new_url = "https://newtest.atlassian.net/"
new_token = "new_token"
self.connector.set_credentials(new_url, new_token)
new_email = "new@example.com"
new_token = "new_api_token"
self.connector.set_credentials(new_url, new_email, new_token)
self.assertEqual(self.connector.base_url, "https://newtest.atlassian.net")
self.assertEqual(self.connector.personal_access_token, new_token)
self.assertEqual(self.connector.email, new_email)
self.assertEqual(self.connector.api_token, new_token)
def test_get_headers(self):
"""Test header generation."""
headers = self.connector.get_headers()
self.assertIn('Content-Type', headers)
self.assertIn('Authorization', headers)
self.assertIn('Accept', headers)
self.assertEqual(headers['Content-Type'], 'application/json')
self.assertEqual(headers['Accept'], 'application/json')
self.assertTrue(headers['Authorization'].startswith('Bearer '))
self.assertIn("Content-Type", headers)
self.assertIn("Authorization", headers)
self.assertIn("Accept", headers)
self.assertEqual(headers["Content-Type"], "application/json")
self.assertEqual(headers["Accept"], "application/json")
self.assertTrue(headers["Authorization"].startswith("Basic "))
def test_get_headers_no_credentials(self):
"""Test header generation without credentials."""
connector = JiraConnector()
with self.assertRaises(ValueError) as context:
connector.get_headers()
self.assertIn("Jira credentials not initialized", str(context.exception))
@patch('requests.get')
@patch("requests.get")
def test_make_api_request_success(self, mock_get):
"""Test successful API request."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"test": "data"}
mock_get.return_value = mock_response
result = self.connector.make_api_request("test/endpoint")
self.assertEqual(result, {"test": "data"})
mock_get.assert_called_once()
@patch('requests.get')
@patch("requests.get")
def test_make_api_request_failure(self, mock_get):
"""Test failed API request."""
mock_response = Mock()
mock_response.status_code = 401
mock_response.text = "Unauthorized"
mock_get.return_value = mock_response
with self.assertRaises(Exception) as context:
self.connector.make_api_request("test/endpoint")
self.assertIn("API request failed with status code 401", str(context.exception))
@patch.object(JiraConnector, 'make_api_request')
@patch.object(JiraConnector, "make_api_request")
def test_get_all_projects(self, mock_api_request):
"""Test getting all projects."""
mock_api_request.return_value = {
"values": [
{"id": "1", "key": "TEST", "name": "Test Project"},
{"id": "2", "key": "DEMO", "name": "Demo Project"}
{"id": "2", "key": "DEMO", "name": "Demo Project"},
]
}
projects = self.connector.get_all_projects()
self.assertEqual(len(projects), 2)
self.assertEqual(projects[0]["key"], "TEST")
self.assertEqual(projects[1]["key"], "DEMO")
mock_api_request.assert_called_once_with("project")
@patch.object(JiraConnector, 'make_api_request')
@patch.object(JiraConnector, "make_api_request")
def test_get_all_issues(self, mock_api_request):
"""Test getting all issues."""
mock_api_request.return_value = {
@ -114,15 +122,15 @@ class TestJiraConnector(unittest.TestCase):
"issuetype": {"name": "Bug"},
"project": {"key": "TEST"},
"created": "2023-01-01T10:00:00.000+0000",
"updated": "2023-01-01T12:00:00.000+0000"
}
"updated": "2023-01-01T12:00:00.000+0000",
},
}
],
"total": 1
"total": 1,
}
issues = self.connector.get_all_issues()
self.assertEqual(len(issues), 1)
self.assertEqual(issues[0]["key"], "TEST-1")
self.assertEqual(issues[0]["fields"]["summary"], "Test Issue")
@ -144,18 +152,18 @@ class TestJiraConnector(unittest.TestCase):
"reporter": {
"accountId": "123",
"displayName": "John Doe",
"emailAddress": "john@example.com"
"emailAddress": "john@example.com",
},
"assignee": {
"accountId": "456",
"displayName": "Jane Smith",
"emailAddress": "jane@example.com"
}
}
"emailAddress": "jane@example.com",
},
},
}
formatted = self.connector.format_issue(raw_issue)
self.assertEqual(formatted["id"], "1")
self.assertEqual(formatted["key"], "TEST-1")
self.assertEqual(formatted["title"], "Test Issue")
@ -170,17 +178,17 @@ class TestJiraConnector(unittest.TestCase):
"""Test date formatting."""
iso_date = "2023-01-01T10:30:00.000+0000"
formatted_date = JiraConnector.format_date(iso_date)
self.assertEqual(formatted_date, "2023-01-01 10:30:00")
def test_format_date_invalid(self):
"""Test date formatting with invalid input."""
formatted_date = JiraConnector.format_date("invalid-date")
self.assertEqual(formatted_date, "invalid-date")
formatted_date = JiraConnector.format_date("")
self.assertEqual(formatted_date, "Unknown date")
formatted_date = JiraConnector.format_date(None)
self.assertEqual(formatted_date, "Unknown date")
@ -198,11 +206,11 @@ class TestJiraConnector(unittest.TestCase):
"created_at": "2023-01-01T10:00:00.000+0000",
"updated_at": "2023-01-01T12:00:00.000+0000",
"description": "Test Description",
"comments": []
"comments": [],
}
markdown = self.connector.format_issue_to_markdown(formatted_issue)
self.assertIn("# TEST-1: Test Issue", markdown)
self.assertIn("**Status:** Open", markdown)
self.assertIn("**Priority:** High", markdown)
@ -214,5 +222,5 @@ class TestJiraConnector(unittest.TestCase):
self.assertIn("Test Description", markdown)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View file

@ -1,9 +1,12 @@
from datetime import datetime
import uuid
from typing import Dict, Any, Optional
from pydantic import BaseModel, field_validator, ConfigDict
from .base import IDModel, TimestampModel
from datetime import datetime
from typing import Any, Dict, Optional
from app.db import SearchSourceConnectorType
from pydantic import BaseModel, ConfigDict, field_validator
from .base import IDModel, TimestampModel
class SearchSourceConnectorBase(BaseModel):
name: str
@ -11,105 +14,129 @@ class SearchSourceConnectorBase(BaseModel):
is_indexable: bool
last_indexed_at: Optional[datetime] = None
config: Dict[str, Any]
@field_validator('config')
@field_validator("config")
@classmethod
def validate_config_for_connector_type(cls, config: Dict[str, Any], values: Dict[str, Any]) -> Dict[str, Any]:
connector_type = values.data.get('connector_type')
def validate_config_for_connector_type(
cls, config: Dict[str, Any], values: Dict[str, Any]
) -> Dict[str, Any]:
connector_type = values.data.get("connector_type")
if connector_type == SearchSourceConnectorType.SERPER_API:
# For SERPER_API, only allow SERPER_API_KEY
allowed_keys = ["SERPER_API_KEY"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For SERPER_API connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For SERPER_API connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the API key is not empty
if not config.get("SERPER_API_KEY"):
raise ValueError("SERPER_API_KEY cannot be empty")
elif connector_type == SearchSourceConnectorType.TAVILY_API:
# For TAVILY_API, only allow TAVILY_API_KEY
allowed_keys = ["TAVILY_API_KEY"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For TAVILY_API connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For TAVILY_API connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the API key is not empty
if not config.get("TAVILY_API_KEY"):
raise ValueError("TAVILY_API_KEY cannot be empty")
elif connector_type == SearchSourceConnectorType.LINKUP_API:
# For LINKUP_API, only allow LINKUP_API_KEY
allowed_keys = ["LINKUP_API_KEY"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For LINKUP_API connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For LINKUP_API connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the API key is not empty
if not config.get("LINKUP_API_KEY"):
raise ValueError("LINKUP_API_KEY cannot be empty")
elif connector_type == SearchSourceConnectorType.SLACK_CONNECTOR:
# For SLACK_CONNECTOR, only allow SLACK_BOT_TOKEN
allowed_keys = ["SLACK_BOT_TOKEN"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For SLACK_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For SLACK_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the bot token is not empty
if not config.get("SLACK_BOT_TOKEN"):
raise ValueError("SLACK_BOT_TOKEN cannot be empty")
elif connector_type == SearchSourceConnectorType.NOTION_CONNECTOR:
# For NOTION_CONNECTOR, only allow NOTION_INTEGRATION_TOKEN
allowed_keys = ["NOTION_INTEGRATION_TOKEN"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For NOTION_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For NOTION_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the integration token is not empty
if not config.get("NOTION_INTEGRATION_TOKEN"):
raise ValueError("NOTION_INTEGRATION_TOKEN cannot be empty")
elif connector_type == SearchSourceConnectorType.GITHUB_CONNECTOR:
# For GITHUB_CONNECTOR, only allow GITHUB_PAT and repo_full_names
allowed_keys = ["GITHUB_PAT", "repo_full_names"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the token is not empty
if not config.get("GITHUB_PAT"):
raise ValueError("GITHUB_PAT cannot be empty")
# Ensure the repo_full_names is present and is a non-empty list
repo_full_names = config.get("repo_full_names")
if not isinstance(repo_full_names, list) or not repo_full_names:
raise ValueError("repo_full_names must be a non-empty list of strings")
elif connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR:
# For LINEAR_CONNECTOR, only allow LINEAR_API_KEY
allowed_keys = ["LINEAR_API_KEY"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For LINEAR_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For LINEAR_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the token is not empty
if not config.get("LINEAR_API_KEY"):
raise ValueError("LINEAR_API_KEY cannot be empty")
elif connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR:
# For DISCORD_CONNECTOR, only allow DISCORD_BOT_TOKEN
allowed_keys = ["DISCORD_BOT_TOKEN"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For DISCORD_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For DISCORD_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the bot token is not empty
if not config.get("DISCORD_BOT_TOKEN"):
raise ValueError("DISCORD_BOT_TOKEN cannot be empty")
elif connector_type == SearchSourceConnectorType.JIRA_CONNECTOR:
# For JIRA_CONNECTOR, allow JIRA_PERSONAL_ACCESS_TOKEN and JIRA_BASE_URL
allowed_keys = ["JIRA_PERSONAL_ACCESS_TOKEN", "JIRA_BASE_URL"]
# For JIRA_CONNECTOR, require JIRA_EMAIL, JIRA_API_TOKEN and JIRA_BASE_URL
allowed_keys = ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For JIRA_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
raise ValueError(
f"For JIRA_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
)
# Ensure the token is not empty
if not config.get("JIRA_PERSONAL_ACCESS_TOKEN"):
raise ValueError("JIRA_PERSONAL_ACCESS_TOKEN cannot be empty")
# Ensure the email is not empty
if not config.get("JIRA_EMAIL"):
raise ValueError("JIRA_EMAIL cannot be empty")
# Ensure the API token is not empty
if not config.get("JIRA_API_TOKEN"):
raise ValueError("JIRA_API_TOKEN cannot be empty")
# Ensure the base URL is not empty
if not config.get("JIRA_BASE_URL"):
@ -117,9 +144,11 @@ class SearchSourceConnectorBase(BaseModel):
return config
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
pass
class SearchSourceConnectorUpdate(BaseModel):
name: Optional[str] = None
connector_type: Optional[SearchSourceConnectorType] = None
@ -127,7 +156,8 @@ class SearchSourceConnectorUpdate(BaseModel):
last_indexed_at: Optional[datetime] = None
config: Optional[Dict[str, Any]] = None
class SearchSourceConnectorRead(SearchSourceConnectorBase, IDModel, TimestampModel):
user_id: uuid.UUID
model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)

View file

@ -1,4 +1,5 @@
import asyncio
import json
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional, Tuple
@ -2041,10 +2042,11 @@ async def index_jira_issues(
return 0, f"Connector with ID {connector_id} not found"
# Get the Jira credentials from the connector config
jira_token = connector.config.get("JIRA_PERSONAL_ACCESS_TOKEN")
jira_email = connector.config.get("JIRA_EMAIL")
jira_api_token = connector.config.get("JIRA_API_TOKEN")
jira_base_url = connector.config.get("JIRA_BASE_URL")
if not jira_token or not jira_base_url:
if not jira_email or not jira_api_token or not jira_base_url:
await task_logger.log_task_failure(
log_entry,
f"Jira credentials not found in connector config for connector {connector_id}",
@ -2061,7 +2063,7 @@ async def index_jira_issues(
)
jira_client = JiraConnector(
base_url=jira_base_url, personal_access_token=jira_token
base_url=jira_base_url, email=jira_email, api_token=jira_api_token
)
# Calculate date range
@ -2097,6 +2099,8 @@ async def index_jira_issues(
start_date=start_date_str, end_date=end_date_str, include_comments=True
)
print(json.dumps(issues, indent=2))
if error:
logger.error(f"Failed to get Jira issues: {error}")
@ -2112,10 +2116,10 @@ async def index_jira_issues(
f"Updated last_indexed_at to {connector.last_indexed_at} despite no issues found"
)
await task_logger.log_task_completion(
await task_logger.log_task_success(
log_entry,
f"No Jira issues found in date range {start_date_str} to {end_date_str}",
{"indexed_count": 0},
{"issues_found": 0},
)
return 0, None
else:
@ -2132,7 +2136,7 @@ async def index_jira_issues(
await task_logger.log_task_progress(
log_entry,
f"Retrieved {len(issues)} issues from Jira API",
{"stage": "processing_issues", "issue_count": len(issues)},
{"stage": "processing_issues", "issues_found": len(issues)},
)
except Exception as e:
@ -2254,10 +2258,10 @@ async def index_jira_issues(
await session.commit()
logger.info(f"Updated last_indexed_at to {connector.last_indexed_at}")
await task_logger.log_task_completion(
await task_logger.log_task_success(
log_entry,
f"Successfully indexed {indexed_count} Jira issues",
{"indexed_count": indexed_count},
{"issues_indexed": indexed_count},
)
logger.info(f"Successfully indexed {indexed_count} Jira issues")

View file

@ -193,10 +193,17 @@ export default function EditConnectorPage() {
/>
<EditSimpleTokenForm
control={editForm.control}
fieldName="JIRA_PERSONAL_ACCESS_TOKEN"
fieldLabel="Jira Personal Access Token"
fieldDescription="Update your Jira Personal Access Token if needed."
placeholder="Your Jira Personal Access Token"
fieldName="JIRA_EMAIL"
fieldLabel="Jira Email"
fieldDescription="Update your Atlassian account email if needed."
placeholder="your.email@company.com"
/>
<EditSimpleTokenForm
control={editForm.control}
fieldName="JIRA_API_TOKEN"
fieldLabel="Jira API Token"
fieldDescription="Update your Jira API Token if needed."
placeholder="Your Jira API Token"
/>
</div>
)}

View file

@ -57,8 +57,11 @@ const jiraConnectorFormSchema = z.object({
message: "Please enter a valid Jira instance URL",
},
),
personal_access_token: z.string().min(10, {
message: "Jira Personal Access Token is required and must be valid.",
email: z.string().email({
message: "Please enter a valid email address.",
}),
api_token: z.string().min(10, {
message: "Jira API Token is required and must be valid.",
}),
});
@ -78,7 +81,8 @@ export default function JiraConnectorPage() {
defaultValues: {
name: "Jira Connector",
base_url: "",
personal_access_token: "",
email: "",
api_token: "",
},
});
@ -91,7 +95,8 @@ export default function JiraConnectorPage() {
connector_type: "JIRA_CONNECTOR",
config: {
JIRA_BASE_URL: values.base_url,
JIRA_PERSONAL_ACCESS_TOKEN: values.personal_access_token,
JIRA_EMAIL: values.email,
JIRA_API_TOKEN: values.api_token,
},
is_indexable: true,
last_indexed_at: null,
@ -210,20 +215,40 @@ export default function JiraConnectorPage() {
<FormField
control={form.control}
name="personal_access_token"
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Personal Access Token</FormLabel>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your Jira Personal Access Token"
type="email"
placeholder="your.email@company.com"
{...field}
/>
</FormControl>
<FormDescription>
Your Jira Personal Access Token will be encrypted
and stored securely.
Your Atlassian account email address.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your Jira API Token"
{...field}
/>
</FormControl>
<FormDescription>
Your Jira API Token will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</FormItem>
@ -296,8 +321,8 @@ export default function JiraConnectorPage() {
<div>
<h3 className="text-xl font-semibold mb-2">How it works</h3>
<p className="text-muted-foreground">
The Jira connector uses the Jira REST API to fetch all
issues and comments that the Personal Access Token has
The Jira connector uses the Jira REST API with Basic Authentication
to fetch all issues and comments that your account has
access to within your Jira instance.
</p>
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
@ -324,15 +349,14 @@ export default function JiraConnectorPage() {
<AlertTitle>Read-Only Access is Sufficient</AlertTitle>
<AlertDescription>
You only need read access for this connector to work.
The Personal Access Token will only be used to read
your Jira data.
The API Token will only be used to read your Jira data.
</AlertDescription>
</Alert>
<div className="space-y-6">
<div>
<h4 className="font-medium mb-2">
Step 1: Create a Personal Access Token
Step 1: Create an API Token
</h4>
<ol className="list-decimal pl-5 space-y-3">
<li>Log in to your Atlassian account</li>
@ -369,10 +393,10 @@ export default function JiraConnectorPage() {
Step 2: Grant necessary access
</h4>
<p className="text-muted-foreground mb-3">
The Personal Access Token will have access to all
projects and issues that your user account can see.
Make sure your account has appropriate permissions
for the projects you want to index.
The API Token will have access to all projects and
issues that your user account can see. Make sure your
account has appropriate permissions for the projects
you want to index.
</p>
<Alert className="bg-muted">
<Info className="h-4 w-4" />