jira-pat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +218 -0
- package/README.md +64 -0
- package/backend/.env.example +1 -0
- package/backend/__tests__/getJiraClient.test.js +57 -0
- package/backend/__tests__/issues.test.js +565 -0
- package/backend/__tests__/jiraService.test.js +1127 -0
- package/backend/__tests__/projects.test.js +256 -0
- package/backend/coverage/clover.xml +426 -0
- package/backend/coverage/coverage-final.json +4 -0
- package/backend/coverage/lcov-report/base.css +224 -0
- package/backend/coverage/lcov-report/block-navigation.js +87 -0
- package/backend/coverage/lcov-report/favicon.png +0 -0
- package/backend/coverage/lcov-report/index.html +131 -0
- package/backend/coverage/lcov-report/prettify.css +1 -0
- package/backend/coverage/lcov-report/prettify.js +2 -0
- package/backend/coverage/lcov-report/routes/index.html +131 -0
- package/backend/coverage/lcov-report/routes/issues.js.html +823 -0
- package/backend/coverage/lcov-report/routes/projects.js.html +190 -0
- package/backend/coverage/lcov-report/service/index.html +116 -0
- package/backend/coverage/lcov-report/service/jiraService.js.html +1663 -0
- package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/backend/coverage/lcov-report/sorter.js +210 -0
- package/backend/coverage/lcov.info +707 -0
- package/backend/index.js +38 -0
- package/backend/jest.config.js +11 -0
- package/backend/package-lock.json +5636 -0
- package/backend/package.json +28 -0
- package/backend/routes/issues.js +246 -0
- package/backend/routes/projects.js +35 -0
- package/backend/service/jiraService.js +526 -0
- package/bin/jira.js +92 -0
- package/frontend/.env.example +1 -0
- package/frontend/coverage/base.css +224 -0
- package/frontend/coverage/block-navigation.js +87 -0
- package/frontend/coverage/clover.xml +559 -0
- package/frontend/coverage/components/CreateIssueModal.jsx.html +592 -0
- package/frontend/coverage/components/IssueDetailPanel.jsx.html +1633 -0
- package/frontend/coverage/components/IssueDrawer.jsx.html +550 -0
- package/frontend/coverage/components/IssueTable.jsx.html +571 -0
- package/frontend/coverage/components/SkeletonComponents.jsx.html +223 -0
- package/frontend/coverage/components/ToastContainer.jsx.html +142 -0
- package/frontend/coverage/components/index.html +191 -0
- package/frontend/coverage/coverage-final.json +14 -0
- package/frontend/coverage/favicon.png +0 -0
- package/frontend/coverage/hooks/index.html +161 -0
- package/frontend/coverage/hooks/useFocusTrap.js.html +262 -0
- package/frontend/coverage/hooks/useIssueDrawer.js.html +1000 -0
- package/frontend/coverage/hooks/useIssuesList.js.html +175 -0
- package/frontend/coverage/hooks/useToasts.js.html +142 -0
- package/frontend/coverage/index.html +161 -0
- package/frontend/coverage/prettify.css +1 -0
- package/frontend/coverage/prettify.js +2 -0
- package/frontend/coverage/services/api.js.html +547 -0
- package/frontend/coverage/services/index.html +116 -0
- package/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/frontend/coverage/sorter.js +210 -0
- package/frontend/coverage/utils/index.html +131 -0
- package/frontend/coverage/utils/issueHelpers.jsx.html +334 -0
- package/frontend/coverage/utils/sanitize.js.html +166 -0
- package/frontend/index.html +13 -0
- package/frontend/package-lock.json +3436 -0
- package/frontend/package.json +30 -0
- package/frontend/src/App.jsx +447 -0
- package/frontend/src/__tests__/components/CreateIssueModal.test.jsx +375 -0
- package/frontend/src/__tests__/components/IssueDetailPanel.test.jsx +962 -0
- package/frontend/src/__tests__/components/IssueDrawer.test.jsx +240 -0
- package/frontend/src/__tests__/components/IssueTable.test.jsx +423 -0
- package/frontend/src/__tests__/components/ToastContainer.test.jsx +196 -0
- package/frontend/src/__tests__/hooks/useFocusTrap.test.js +197 -0
- package/frontend/src/__tests__/hooks/useIssueDrawer.test.js +1053 -0
- package/frontend/src/__tests__/hooks/useIssuesList.test.js +175 -0
- package/frontend/src/__tests__/hooks/useToasts.test.js +110 -0
- package/frontend/src/__tests__/services/api.test.js +568 -0
- package/frontend/src/__tests__/setup.js +54 -0
- package/frontend/src/__tests__/utils/issueHelpers.test.jsx +336 -0
- package/frontend/src/__tests__/utils/sanitize.test.js +238 -0
- package/frontend/src/components/CreateIssueModal.jsx +169 -0
- package/frontend/src/components/ErrorBoundary.jsx +52 -0
- package/frontend/src/components/IssueDetailPanel.jsx +517 -0
- package/frontend/src/components/IssueDrawer.jsx +155 -0
- package/frontend/src/components/IssueTable.jsx +162 -0
- package/frontend/src/components/SkeletonComponents.jsx +46 -0
- package/frontend/src/components/StandaloneIssuePage.jsx +176 -0
- package/frontend/src/components/ToastContainer.jsx +19 -0
- package/frontend/src/hooks/useFocusTrap.js +59 -0
- package/frontend/src/hooks/useIssueDrawer.js +305 -0
- package/frontend/src/hooks/useIssuesList.js +30 -0
- package/frontend/src/hooks/useToasts.js +19 -0
- package/frontend/src/index.css +2070 -0
- package/frontend/src/main.jsx +13 -0
- package/frontend/src/services/api.js +154 -0
- package/frontend/src/utils/issueHelpers.jsx +84 -0
- package/frontend/src/utils/sanitize.js +27 -0
- package/frontend/vite.config.js +15 -0
- package/package.json +19 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index.js",
|
|
8
|
+
"test": "jest",
|
|
9
|
+
"test:coverage": "jest --coverage"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"axios": "^1.13.6",
|
|
16
|
+
"cors": "^2.8.6",
|
|
17
|
+
"dotenv": "^17.3.1",
|
|
18
|
+
"express": "^5.2.1",
|
|
19
|
+
"express-rate-limit": "^8.3.1",
|
|
20
|
+
"form-data": "^4.0.5",
|
|
21
|
+
"multer": "^2.1.1",
|
|
22
|
+
"node-cache": "^5.1.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"jest": "^30.3.0",
|
|
26
|
+
"supertest": "^7.2.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const multer = require('multer');
|
|
3
|
+
const rateLimit = require('express-rate-limit');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const jiraService = require('../service/jiraService');
|
|
6
|
+
|
|
7
|
+
const upload = multer({
|
|
8
|
+
storage: multer.memoryStorage(),
|
|
9
|
+
limits: { fileSize: 10 * 1024 * 1024 }
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const mutationLimiter = rateLimit({
|
|
13
|
+
windowMs: 60 * 1000,
|
|
14
|
+
max: 10,
|
|
15
|
+
standardHeaders: true,
|
|
16
|
+
legacyHeaders: false,
|
|
17
|
+
message: { error: 'Too many requests, please slow down.' },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const ISSUE_KEY_REGEX = /^[A-Z]+-\d+$/;
|
|
21
|
+
const DANGEROUS_CHARS = /['";\\-]|--/g;
|
|
22
|
+
|
|
23
|
+
const escapeJql = (str) => String(str).replace(/'/g, "\\'");
|
|
24
|
+
|
|
25
|
+
const validateIssueKey = (key) => {
|
|
26
|
+
if (!ISSUE_KEY_REGEX.test(key)) {
|
|
27
|
+
return 'Invalid issue key format. Expected format: PROJECT-123';
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// GET /api/issues
|
|
33
|
+
router.get('/', async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
if (req.query.search) {
|
|
36
|
+
req.query.search = String(req.query.search).replace(DANGEROUS_CHARS, '');
|
|
37
|
+
}
|
|
38
|
+
const data = await jiraService.searchIssues(req.query);
|
|
39
|
+
|
|
40
|
+
if (!data || !data.issues || data.issues.length === 0) {
|
|
41
|
+
return res.status(200).json({ issues: [], total: 0, message: 'No issues found' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
res.json(data);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error fetching issues:', error.message);
|
|
47
|
+
res.status(500).json({
|
|
48
|
+
error: 'Failed to fetch issues',
|
|
49
|
+
details: error.message
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// POST /api/issues
|
|
55
|
+
router.post('/', mutationLimiter, async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
if (!req.body.summary || typeof req.body.summary !== 'string') {
|
|
58
|
+
return res.status(400).json({ error: 'Summary is required' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const summary = req.body.summary.trim();
|
|
62
|
+
if (summary.length === 0) {
|
|
63
|
+
return res.status(400).json({ error: 'Summary cannot be empty' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (summary.length > 255) {
|
|
67
|
+
return res.status(400).json({ error: 'Summary must be under 255 characters' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
req.body.summary = summary;
|
|
71
|
+
const newIssue = await jiraService.createIssue(req.body);
|
|
72
|
+
res.status(201).json(newIssue);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
res.status(500).json({ error: 'Failed to create issue', details: error.message });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// GET /api/issues/labels
|
|
79
|
+
router.get('/labels/search', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const { query } = req.query;
|
|
82
|
+
const labels = await jiraService.searchLabels(query);
|
|
83
|
+
res.json(labels);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
res.status(500).json({ error: 'Failed to search labels', details: error.message });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// GET /api/issues/image/proxy
|
|
90
|
+
router.get('/image/proxy', async (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
const { url } = req.query;
|
|
93
|
+
if (!url) return res.status(400).send('URL is required');
|
|
94
|
+
|
|
95
|
+
const remoteResponse = await jiraService.downloadAttachment(url);
|
|
96
|
+
// Relay headers (content-type)
|
|
97
|
+
if (remoteResponse.headers['content-type']) {
|
|
98
|
+
res.set('Content-Type', remoteResponse.headers['content-type']);
|
|
99
|
+
}
|
|
100
|
+
// Browser-only cache for 24 hours
|
|
101
|
+
res.set('Cache-Control', 'public, max-age=86400');
|
|
102
|
+
remoteResponse.data.pipe(res);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error proxying image:', error.message);
|
|
105
|
+
res.status(500).send('Failed to fetch image');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// GET /api/issues/:issueKey
|
|
110
|
+
router.get('/:issueKey', async (req, res) => {
|
|
111
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
112
|
+
if (error) return res.status(400).json({ error });
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const issue = await jiraService.getIssueDetail(req.params.issueKey);
|
|
116
|
+
res.json(issue);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
res.status(500).json({ error: 'Failed to fetch issue details', details: error.message });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// GET /api/issues/:issueKey/comments
|
|
123
|
+
router.get('/:issueKey/comments', async (req, res) => {
|
|
124
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
125
|
+
if (error) return res.status(400).json({ error });
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const comments = await jiraService.getComments(req.params.issueKey);
|
|
129
|
+
res.json(comments);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
res.status(500).json({ error: 'Failed to fetch comments', details: error.message });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// POST /api/issues/:issueKey/comments
|
|
136
|
+
router.post('/:issueKey/comments', mutationLimiter, async (req, res) => {
|
|
137
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
138
|
+
if (error) return res.status(400).json({ error });
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const { body } = req.body;
|
|
142
|
+
if (!body || !body.trim()) {
|
|
143
|
+
return res.status(400).json({ error: 'Comment body cannot be empty' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const comment = await jiraService.addComment(req.params.issueKey, body);
|
|
147
|
+
res.status(201).json(comment);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
res.status(500).json({ error: 'Failed to post comment', details: error.message });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// GET /api/issues/:issueKey/transitions
|
|
154
|
+
router.get('/:issueKey/transitions', async (req, res) => {
|
|
155
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
156
|
+
if (error) return res.status(400).json({ error });
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const transitions = await jiraService.getTransitions(req.params.issueKey);
|
|
160
|
+
res.json(transitions);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
res.status(500).json({ error: 'Failed to fetch transitions', details: error.message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// POST /api/issues/:issueKey/transitions
|
|
167
|
+
router.post('/:issueKey/transitions', mutationLimiter, async (req, res) => {
|
|
168
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
169
|
+
if (error) return res.status(400).json({ error });
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const { transitionId } = req.body;
|
|
173
|
+
if (!transitionId) return res.status(400).json({ error: 'transitionId is required' });
|
|
174
|
+
await jiraService.transitionIssue(req.params.issueKey, transitionId);
|
|
175
|
+
res.json({ success: true });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
res.status(500).json({ error: 'Failed to transition issue', details: error.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// GET /api/issues/:issueKey/assignable
|
|
182
|
+
router.get('/:issueKey/assignable', async (req, res) => {
|
|
183
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
184
|
+
if (error) return res.status(400).json({ error });
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const { query } = req.query;
|
|
188
|
+
const users = await jiraService.getAssignableUsers(req.params.issueKey, query);
|
|
189
|
+
res.json(users);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
res.status(500).json({ error: 'Failed to fetch assignable users', details: error.message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// PUT /api/issues/:issueKey/assignee
|
|
196
|
+
router.put('/:issueKey/assignee', mutationLimiter, async (req, res) => {
|
|
197
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
198
|
+
if (error) return res.status(400).json({ error });
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const { accountId } = req.body;
|
|
202
|
+
await jiraService.assignIssue(req.params.issueKey, accountId);
|
|
203
|
+
res.json({ success: true });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
res.status(500).json({ error: 'Failed to assign issue', details: error.message });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// POST /api/issues/:issueKey/attachments
|
|
210
|
+
router.post('/:issueKey/attachments', mutationLimiter, upload.single('file'), async (req, res) => {
|
|
211
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
212
|
+
if (error) return res.status(400).json({ error });
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
if (!req.file) {
|
|
216
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const attachments = await jiraService.attachFile(
|
|
220
|
+
req.params.issueKey,
|
|
221
|
+
req.file.buffer,
|
|
222
|
+
req.file.originalname,
|
|
223
|
+
req.file.mimetype
|
|
224
|
+
);
|
|
225
|
+
res.status(201).json(attachments);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
res.status(500).json({ error: 'Failed to upload attachment', details: error.message });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// PUT /api/issues/:issueKey
|
|
232
|
+
router.put('/:issueKey', mutationLimiter, async (req, res) => {
|
|
233
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
234
|
+
if (error) return res.status(400).json({ error });
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const { fields } = req.body;
|
|
238
|
+
if (!fields) return res.status(400).json({ error: 'fields are required' });
|
|
239
|
+
await jiraService.updateIssue(req.params.issueKey, fields);
|
|
240
|
+
res.json({ success: true });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
res.status(500).json({ error: 'Failed to update issue', details: error.message });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
module.exports = router;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const jiraService = require('../service/jiraService');
|
|
4
|
+
|
|
5
|
+
// GET /api/projects
|
|
6
|
+
router.get('/', async (req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
const projects = await jiraService.getProjects();
|
|
9
|
+
res.json(projects);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
res.status(500).json({ error: 'Failed to fetch projects', details: error.message });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// GET /api/projects/:projectKey/issuetypes
|
|
16
|
+
router.get('/:projectKey/issuetypes', async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const types = await jiraService.getIssueTypes(req.params.projectKey);
|
|
19
|
+
res.json(types);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
res.status(500).json({ error: 'Failed to fetch issue types', details: error.message });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// GET /api/projects/:projectKey/versions
|
|
26
|
+
router.get('/:projectKey/versions', async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const versions = await jiraService.getProjectVersions(req.params.projectKey);
|
|
29
|
+
res.json(versions);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
res.status(500).json({ error: 'Failed to fetch versions', details: error.message });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
module.exports = router;
|