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.
Files changed (95) hide show
  1. package/AGENTS.md +218 -0
  2. package/README.md +64 -0
  3. package/backend/.env.example +1 -0
  4. package/backend/__tests__/getJiraClient.test.js +57 -0
  5. package/backend/__tests__/issues.test.js +565 -0
  6. package/backend/__tests__/jiraService.test.js +1127 -0
  7. package/backend/__tests__/projects.test.js +256 -0
  8. package/backend/coverage/clover.xml +426 -0
  9. package/backend/coverage/coverage-final.json +4 -0
  10. package/backend/coverage/lcov-report/base.css +224 -0
  11. package/backend/coverage/lcov-report/block-navigation.js +87 -0
  12. package/backend/coverage/lcov-report/favicon.png +0 -0
  13. package/backend/coverage/lcov-report/index.html +131 -0
  14. package/backend/coverage/lcov-report/prettify.css +1 -0
  15. package/backend/coverage/lcov-report/prettify.js +2 -0
  16. package/backend/coverage/lcov-report/routes/index.html +131 -0
  17. package/backend/coverage/lcov-report/routes/issues.js.html +823 -0
  18. package/backend/coverage/lcov-report/routes/projects.js.html +190 -0
  19. package/backend/coverage/lcov-report/service/index.html +116 -0
  20. package/backend/coverage/lcov-report/service/jiraService.js.html +1663 -0
  21. package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  22. package/backend/coverage/lcov-report/sorter.js +210 -0
  23. package/backend/coverage/lcov.info +707 -0
  24. package/backend/index.js +38 -0
  25. package/backend/jest.config.js +11 -0
  26. package/backend/package-lock.json +5636 -0
  27. package/backend/package.json +28 -0
  28. package/backend/routes/issues.js +246 -0
  29. package/backend/routes/projects.js +35 -0
  30. package/backend/service/jiraService.js +526 -0
  31. package/bin/jira.js +92 -0
  32. package/frontend/.env.example +1 -0
  33. package/frontend/coverage/base.css +224 -0
  34. package/frontend/coverage/block-navigation.js +87 -0
  35. package/frontend/coverage/clover.xml +559 -0
  36. package/frontend/coverage/components/CreateIssueModal.jsx.html +592 -0
  37. package/frontend/coverage/components/IssueDetailPanel.jsx.html +1633 -0
  38. package/frontend/coverage/components/IssueDrawer.jsx.html +550 -0
  39. package/frontend/coverage/components/IssueTable.jsx.html +571 -0
  40. package/frontend/coverage/components/SkeletonComponents.jsx.html +223 -0
  41. package/frontend/coverage/components/ToastContainer.jsx.html +142 -0
  42. package/frontend/coverage/components/index.html +191 -0
  43. package/frontend/coverage/coverage-final.json +14 -0
  44. package/frontend/coverage/favicon.png +0 -0
  45. package/frontend/coverage/hooks/index.html +161 -0
  46. package/frontend/coverage/hooks/useFocusTrap.js.html +262 -0
  47. package/frontend/coverage/hooks/useIssueDrawer.js.html +1000 -0
  48. package/frontend/coverage/hooks/useIssuesList.js.html +175 -0
  49. package/frontend/coverage/hooks/useToasts.js.html +142 -0
  50. package/frontend/coverage/index.html +161 -0
  51. package/frontend/coverage/prettify.css +1 -0
  52. package/frontend/coverage/prettify.js +2 -0
  53. package/frontend/coverage/services/api.js.html +547 -0
  54. package/frontend/coverage/services/index.html +116 -0
  55. package/frontend/coverage/sort-arrow-sprite.png +0 -0
  56. package/frontend/coverage/sorter.js +210 -0
  57. package/frontend/coverage/utils/index.html +131 -0
  58. package/frontend/coverage/utils/issueHelpers.jsx.html +334 -0
  59. package/frontend/coverage/utils/sanitize.js.html +166 -0
  60. package/frontend/index.html +13 -0
  61. package/frontend/package-lock.json +3436 -0
  62. package/frontend/package.json +30 -0
  63. package/frontend/src/App.jsx +447 -0
  64. package/frontend/src/__tests__/components/CreateIssueModal.test.jsx +375 -0
  65. package/frontend/src/__tests__/components/IssueDetailPanel.test.jsx +962 -0
  66. package/frontend/src/__tests__/components/IssueDrawer.test.jsx +240 -0
  67. package/frontend/src/__tests__/components/IssueTable.test.jsx +423 -0
  68. package/frontend/src/__tests__/components/ToastContainer.test.jsx +196 -0
  69. package/frontend/src/__tests__/hooks/useFocusTrap.test.js +197 -0
  70. package/frontend/src/__tests__/hooks/useIssueDrawer.test.js +1053 -0
  71. package/frontend/src/__tests__/hooks/useIssuesList.test.js +175 -0
  72. package/frontend/src/__tests__/hooks/useToasts.test.js +110 -0
  73. package/frontend/src/__tests__/services/api.test.js +568 -0
  74. package/frontend/src/__tests__/setup.js +54 -0
  75. package/frontend/src/__tests__/utils/issueHelpers.test.jsx +336 -0
  76. package/frontend/src/__tests__/utils/sanitize.test.js +238 -0
  77. package/frontend/src/components/CreateIssueModal.jsx +169 -0
  78. package/frontend/src/components/ErrorBoundary.jsx +52 -0
  79. package/frontend/src/components/IssueDetailPanel.jsx +517 -0
  80. package/frontend/src/components/IssueDrawer.jsx +155 -0
  81. package/frontend/src/components/IssueTable.jsx +162 -0
  82. package/frontend/src/components/SkeletonComponents.jsx +46 -0
  83. package/frontend/src/components/StandaloneIssuePage.jsx +176 -0
  84. package/frontend/src/components/ToastContainer.jsx +19 -0
  85. package/frontend/src/hooks/useFocusTrap.js +59 -0
  86. package/frontend/src/hooks/useIssueDrawer.js +305 -0
  87. package/frontend/src/hooks/useIssuesList.js +30 -0
  88. package/frontend/src/hooks/useToasts.js +19 -0
  89. package/frontend/src/index.css +2070 -0
  90. package/frontend/src/main.jsx +13 -0
  91. package/frontend/src/services/api.js +154 -0
  92. package/frontend/src/utils/issueHelpers.jsx +84 -0
  93. package/frontend/src/utils/sanitize.js +27 -0
  94. package/frontend/vite.config.js +15 -0
  95. 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;