diff --git a/backend/routes/jiraTickets.js b/backend/routes/jiraTickets.js index bc20949..d7e9816 100644 --- a/backend/routes/jiraTickets.js +++ b/backend/routes/jiraTickets.js @@ -522,11 +522,21 @@ function createJiraTicketsRouter() { router.post('/', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => { const { cve_id, vendor, ticket_key, url, summary, status } = req.body; - if (!cve_id || !isValidCveId(cve_id)) { - return res.status(400).json({ error: 'Valid CVE ID is required.' }); + // CVE ID is optional — validate format only if provided and non-empty + let normalizedCveId = null; + if (cve_id && typeof cve_id === 'string' && cve_id.trim().length > 0) { + if (!isValidCveId(cve_id)) { + return res.status(400).json({ error: 'CVE ID format is invalid. Expected CVE-YYYY-NNNN+.' }); + } + normalizedCveId = cve_id; } - if (!vendor || !isValidVendor(vendor)) { - return res.status(400).json({ error: 'Valid vendor is required.' }); + // Vendor is optional — validate length only if provided and non-empty + let normalizedVendor = null; + if (vendor && typeof vendor === 'string' && vendor.trim().length > 0) { + if (vendor.trim().length > 200) { + return res.status(400).json({ error: 'Vendor exceeds maximum length of 200 characters.' }); + } + normalizedVendor = vendor.trim(); } if (!ticket_key || typeof ticket_key !== 'string' || ticket_key.trim().length === 0 || ticket_key.length > 50) { return res.status(400).json({ error: 'Ticket key is required (max 50 chars).' }); @@ -548,7 +558,7 @@ function createJiraTicketsRouter() { `INSERT INTO jira_tickets (cve_id, vendor, ticket_key, url, summary, status, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, - [cve_id, vendor, ticket_key.trim(), url || null, summary || null, ticketStatus, req.user.id] + [normalizedCveId, normalizedVendor, ticket_key.trim(), url || null, summary || null, ticketStatus, req.user.id] ); logAudit({ @@ -557,7 +567,7 @@ function createJiraTicketsRouter() { action: 'jira_ticket_create', entityType: 'jira_ticket', entityId: rows[0].id.toString(), - details: { cve_id, vendor, ticket_key, status: ticketStatus }, + details: { cve_id: normalizedCveId, vendor: normalizedVendor, ticket_key, status: ticketStatus }, ipAddress: req.ip }); diff --git a/frontend/src/components/pages/JiraPage.js b/frontend/src/components/pages/JiraPage.js index 5e954c7..d49f691 100644 --- a/frontend/src/components/pages/JiraPage.js +++ b/frontend/src/components/pages/JiraPage.js @@ -182,6 +182,9 @@ export default function JiraPage() { const [lookupResult, setLookupResult] = useState(null); const [lookupLoading, setLookupLoading] = useState(false); const [lookupError, setLookupError] = useState(null); + const [linkingSaving, setLinkingSaving] = useState(false); + const [linkingError, setLinkingError] = useState(null); + const [linkingSuccess, setLinkingSuccess] = useState(null); // Add/Edit modal const [showForm, setShowForm] = useState(false); @@ -287,6 +290,37 @@ export default function JiraPage() { } }; + // --------------------------------------------------------------------------- + // Link existing Jira ticket — save to local DB without recreating in Jira + // --------------------------------------------------------------------------- + const linkExistingTicket = async (issue) => { + setLinkingError(null); + setLinkingSuccess(null); + setLinkingSaving(true); + try { + const jiraUrl = `https://jira.charter.com/browse/${issue.key}`; + const res = await fetch(`${API_BASE}/jira-tickets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + ticket_key: issue.key, + url: jiraUrl, + summary: issue.summary || '', + status: issue.status === 'Open' || issue.status === 'In Progress' || issue.status === 'Closed' ? issue.status : 'Open', + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + setLinkingSuccess(`${issue.key} saved to dashboard.`); + fetchTickets(); + } catch (err) { + setLinkingError(err.message); + } finally { + setLinkingSaving(false); + } + }; + // --------------------------------------------------------------------------- // CRUD — save (create or update) @@ -715,6 +749,18 @@ export default function JiraPage() {
Priority: {lookupResult.priority}
Assignee: {lookupResult.assignee || 'Unassigned'}
Updated: {lookupResult.updated ? new Date(lookupResult.updated).toLocaleString() : '-'}
+ {canWrite() && ( + + )} + {linkingError &&
{linkingError}
} + {linkingSuccess &&
{linkingSuccess}
} )} @@ -828,8 +874,16 @@ export default function JiraPage() { setCreateJiraForm(f => ({ ...f, project_key: e.target.value.toUpperCase() }))} />
- - setCreateJiraForm(f => ({ ...f, issue_type: e.target.value }))} /> + +