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,823 @@
1
+
2
+ <!doctype html>
3
+ <html lang="en">
4
+
5
+ <head>
6
+ <title>Code coverage report for routes/issues.js</title>
7
+ <meta charset="utf-8" />
8
+ <link rel="stylesheet" href="../prettify.css" />
9
+ <link rel="stylesheet" href="../base.css" />
10
+ <link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
12
+ <style type='text/css'>
13
+ .coverage-summary .sorter {
14
+ background-image: url(../sort-arrow-sprite.png);
15
+ }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <div class='wrapper'>
21
+ <div class='pad1'>
22
+ <h1><a href="../index.html">All files</a> / <a href="index.html">routes</a> issues.js</h1>
23
+ <div class='clearfix'>
24
+
25
+ <div class='fl pad1y space-right2'>
26
+ <span class="strong">99.29% </span>
27
+ <span class="quiet">Statements</span>
28
+ <span class='fraction'>141/142</span>
29
+ </div>
30
+
31
+
32
+ <div class='fl pad1y space-right2'>
33
+ <span class="strong">100% </span>
34
+ <span class="quiet">Branches</span>
35
+ <span class='fraction'>49/49</span>
36
+ </div>
37
+
38
+
39
+ <div class='fl pad1y space-right2'>
40
+ <span class="strong">93.33% </span>
41
+ <span class="quiet">Functions</span>
42
+ <span class='fraction'>14/15</span>
43
+ </div>
44
+
45
+
46
+ <div class='fl pad1y space-right2'>
47
+ <span class="strong">100% </span>
48
+ <span class="quiet">Lines</span>
49
+ <span class='fraction'>129/129</span>
50
+ </div>
51
+
52
+
53
+ </div>
54
+ <p class="quiet">
55
+ Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
56
+ </p>
57
+ <template id="filterTemplate">
58
+ <div class="quiet">
59
+ Filter:
60
+ <input type="search" id="fileSearch">
61
+ </div>
62
+ </template>
63
+ </div>
64
+ <div class='status-line high'></div>
65
+ <pre><table class="coverage">
66
+ <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
67
+ <a name='L2'></a><a href='#L2'>2</a>
68
+ <a name='L3'></a><a href='#L3'>3</a>
69
+ <a name='L4'></a><a href='#L4'>4</a>
70
+ <a name='L5'></a><a href='#L5'>5</a>
71
+ <a name='L6'></a><a href='#L6'>6</a>
72
+ <a name='L7'></a><a href='#L7'>7</a>
73
+ <a name='L8'></a><a href='#L8'>8</a>
74
+ <a name='L9'></a><a href='#L9'>9</a>
75
+ <a name='L10'></a><a href='#L10'>10</a>
76
+ <a name='L11'></a><a href='#L11'>11</a>
77
+ <a name='L12'></a><a href='#L12'>12</a>
78
+ <a name='L13'></a><a href='#L13'>13</a>
79
+ <a name='L14'></a><a href='#L14'>14</a>
80
+ <a name='L15'></a><a href='#L15'>15</a>
81
+ <a name='L16'></a><a href='#L16'>16</a>
82
+ <a name='L17'></a><a href='#L17'>17</a>
83
+ <a name='L18'></a><a href='#L18'>18</a>
84
+ <a name='L19'></a><a href='#L19'>19</a>
85
+ <a name='L20'></a><a href='#L20'>20</a>
86
+ <a name='L21'></a><a href='#L21'>21</a>
87
+ <a name='L22'></a><a href='#L22'>22</a>
88
+ <a name='L23'></a><a href='#L23'>23</a>
89
+ <a name='L24'></a><a href='#L24'>24</a>
90
+ <a name='L25'></a><a href='#L25'>25</a>
91
+ <a name='L26'></a><a href='#L26'>26</a>
92
+ <a name='L27'></a><a href='#L27'>27</a>
93
+ <a name='L28'></a><a href='#L28'>28</a>
94
+ <a name='L29'></a><a href='#L29'>29</a>
95
+ <a name='L30'></a><a href='#L30'>30</a>
96
+ <a name='L31'></a><a href='#L31'>31</a>
97
+ <a name='L32'></a><a href='#L32'>32</a>
98
+ <a name='L33'></a><a href='#L33'>33</a>
99
+ <a name='L34'></a><a href='#L34'>34</a>
100
+ <a name='L35'></a><a href='#L35'>35</a>
101
+ <a name='L36'></a><a href='#L36'>36</a>
102
+ <a name='L37'></a><a href='#L37'>37</a>
103
+ <a name='L38'></a><a href='#L38'>38</a>
104
+ <a name='L39'></a><a href='#L39'>39</a>
105
+ <a name='L40'></a><a href='#L40'>40</a>
106
+ <a name='L41'></a><a href='#L41'>41</a>
107
+ <a name='L42'></a><a href='#L42'>42</a>
108
+ <a name='L43'></a><a href='#L43'>43</a>
109
+ <a name='L44'></a><a href='#L44'>44</a>
110
+ <a name='L45'></a><a href='#L45'>45</a>
111
+ <a name='L46'></a><a href='#L46'>46</a>
112
+ <a name='L47'></a><a href='#L47'>47</a>
113
+ <a name='L48'></a><a href='#L48'>48</a>
114
+ <a name='L49'></a><a href='#L49'>49</a>
115
+ <a name='L50'></a><a href='#L50'>50</a>
116
+ <a name='L51'></a><a href='#L51'>51</a>
117
+ <a name='L52'></a><a href='#L52'>52</a>
118
+ <a name='L53'></a><a href='#L53'>53</a>
119
+ <a name='L54'></a><a href='#L54'>54</a>
120
+ <a name='L55'></a><a href='#L55'>55</a>
121
+ <a name='L56'></a><a href='#L56'>56</a>
122
+ <a name='L57'></a><a href='#L57'>57</a>
123
+ <a name='L58'></a><a href='#L58'>58</a>
124
+ <a name='L59'></a><a href='#L59'>59</a>
125
+ <a name='L60'></a><a href='#L60'>60</a>
126
+ <a name='L61'></a><a href='#L61'>61</a>
127
+ <a name='L62'></a><a href='#L62'>62</a>
128
+ <a name='L63'></a><a href='#L63'>63</a>
129
+ <a name='L64'></a><a href='#L64'>64</a>
130
+ <a name='L65'></a><a href='#L65'>65</a>
131
+ <a name='L66'></a><a href='#L66'>66</a>
132
+ <a name='L67'></a><a href='#L67'>67</a>
133
+ <a name='L68'></a><a href='#L68'>68</a>
134
+ <a name='L69'></a><a href='#L69'>69</a>
135
+ <a name='L70'></a><a href='#L70'>70</a>
136
+ <a name='L71'></a><a href='#L71'>71</a>
137
+ <a name='L72'></a><a href='#L72'>72</a>
138
+ <a name='L73'></a><a href='#L73'>73</a>
139
+ <a name='L74'></a><a href='#L74'>74</a>
140
+ <a name='L75'></a><a href='#L75'>75</a>
141
+ <a name='L76'></a><a href='#L76'>76</a>
142
+ <a name='L77'></a><a href='#L77'>77</a>
143
+ <a name='L78'></a><a href='#L78'>78</a>
144
+ <a name='L79'></a><a href='#L79'>79</a>
145
+ <a name='L80'></a><a href='#L80'>80</a>
146
+ <a name='L81'></a><a href='#L81'>81</a>
147
+ <a name='L82'></a><a href='#L82'>82</a>
148
+ <a name='L83'></a><a href='#L83'>83</a>
149
+ <a name='L84'></a><a href='#L84'>84</a>
150
+ <a name='L85'></a><a href='#L85'>85</a>
151
+ <a name='L86'></a><a href='#L86'>86</a>
152
+ <a name='L87'></a><a href='#L87'>87</a>
153
+ <a name='L88'></a><a href='#L88'>88</a>
154
+ <a name='L89'></a><a href='#L89'>89</a>
155
+ <a name='L90'></a><a href='#L90'>90</a>
156
+ <a name='L91'></a><a href='#L91'>91</a>
157
+ <a name='L92'></a><a href='#L92'>92</a>
158
+ <a name='L93'></a><a href='#L93'>93</a>
159
+ <a name='L94'></a><a href='#L94'>94</a>
160
+ <a name='L95'></a><a href='#L95'>95</a>
161
+ <a name='L96'></a><a href='#L96'>96</a>
162
+ <a name='L97'></a><a href='#L97'>97</a>
163
+ <a name='L98'></a><a href='#L98'>98</a>
164
+ <a name='L99'></a><a href='#L99'>99</a>
165
+ <a name='L100'></a><a href='#L100'>100</a>
166
+ <a name='L101'></a><a href='#L101'>101</a>
167
+ <a name='L102'></a><a href='#L102'>102</a>
168
+ <a name='L103'></a><a href='#L103'>103</a>
169
+ <a name='L104'></a><a href='#L104'>104</a>
170
+ <a name='L105'></a><a href='#L105'>105</a>
171
+ <a name='L106'></a><a href='#L106'>106</a>
172
+ <a name='L107'></a><a href='#L107'>107</a>
173
+ <a name='L108'></a><a href='#L108'>108</a>
174
+ <a name='L109'></a><a href='#L109'>109</a>
175
+ <a name='L110'></a><a href='#L110'>110</a>
176
+ <a name='L111'></a><a href='#L111'>111</a>
177
+ <a name='L112'></a><a href='#L112'>112</a>
178
+ <a name='L113'></a><a href='#L113'>113</a>
179
+ <a name='L114'></a><a href='#L114'>114</a>
180
+ <a name='L115'></a><a href='#L115'>115</a>
181
+ <a name='L116'></a><a href='#L116'>116</a>
182
+ <a name='L117'></a><a href='#L117'>117</a>
183
+ <a name='L118'></a><a href='#L118'>118</a>
184
+ <a name='L119'></a><a href='#L119'>119</a>
185
+ <a name='L120'></a><a href='#L120'>120</a>
186
+ <a name='L121'></a><a href='#L121'>121</a>
187
+ <a name='L122'></a><a href='#L122'>122</a>
188
+ <a name='L123'></a><a href='#L123'>123</a>
189
+ <a name='L124'></a><a href='#L124'>124</a>
190
+ <a name='L125'></a><a href='#L125'>125</a>
191
+ <a name='L126'></a><a href='#L126'>126</a>
192
+ <a name='L127'></a><a href='#L127'>127</a>
193
+ <a name='L128'></a><a href='#L128'>128</a>
194
+ <a name='L129'></a><a href='#L129'>129</a>
195
+ <a name='L130'></a><a href='#L130'>130</a>
196
+ <a name='L131'></a><a href='#L131'>131</a>
197
+ <a name='L132'></a><a href='#L132'>132</a>
198
+ <a name='L133'></a><a href='#L133'>133</a>
199
+ <a name='L134'></a><a href='#L134'>134</a>
200
+ <a name='L135'></a><a href='#L135'>135</a>
201
+ <a name='L136'></a><a href='#L136'>136</a>
202
+ <a name='L137'></a><a href='#L137'>137</a>
203
+ <a name='L138'></a><a href='#L138'>138</a>
204
+ <a name='L139'></a><a href='#L139'>139</a>
205
+ <a name='L140'></a><a href='#L140'>140</a>
206
+ <a name='L141'></a><a href='#L141'>141</a>
207
+ <a name='L142'></a><a href='#L142'>142</a>
208
+ <a name='L143'></a><a href='#L143'>143</a>
209
+ <a name='L144'></a><a href='#L144'>144</a>
210
+ <a name='L145'></a><a href='#L145'>145</a>
211
+ <a name='L146'></a><a href='#L146'>146</a>
212
+ <a name='L147'></a><a href='#L147'>147</a>
213
+ <a name='L148'></a><a href='#L148'>148</a>
214
+ <a name='L149'></a><a href='#L149'>149</a>
215
+ <a name='L150'></a><a href='#L150'>150</a>
216
+ <a name='L151'></a><a href='#L151'>151</a>
217
+ <a name='L152'></a><a href='#L152'>152</a>
218
+ <a name='L153'></a><a href='#L153'>153</a>
219
+ <a name='L154'></a><a href='#L154'>154</a>
220
+ <a name='L155'></a><a href='#L155'>155</a>
221
+ <a name='L156'></a><a href='#L156'>156</a>
222
+ <a name='L157'></a><a href='#L157'>157</a>
223
+ <a name='L158'></a><a href='#L158'>158</a>
224
+ <a name='L159'></a><a href='#L159'>159</a>
225
+ <a name='L160'></a><a href='#L160'>160</a>
226
+ <a name='L161'></a><a href='#L161'>161</a>
227
+ <a name='L162'></a><a href='#L162'>162</a>
228
+ <a name='L163'></a><a href='#L163'>163</a>
229
+ <a name='L164'></a><a href='#L164'>164</a>
230
+ <a name='L165'></a><a href='#L165'>165</a>
231
+ <a name='L166'></a><a href='#L166'>166</a>
232
+ <a name='L167'></a><a href='#L167'>167</a>
233
+ <a name='L168'></a><a href='#L168'>168</a>
234
+ <a name='L169'></a><a href='#L169'>169</a>
235
+ <a name='L170'></a><a href='#L170'>170</a>
236
+ <a name='L171'></a><a href='#L171'>171</a>
237
+ <a name='L172'></a><a href='#L172'>172</a>
238
+ <a name='L173'></a><a href='#L173'>173</a>
239
+ <a name='L174'></a><a href='#L174'>174</a>
240
+ <a name='L175'></a><a href='#L175'>175</a>
241
+ <a name='L176'></a><a href='#L176'>176</a>
242
+ <a name='L177'></a><a href='#L177'>177</a>
243
+ <a name='L178'></a><a href='#L178'>178</a>
244
+ <a name='L179'></a><a href='#L179'>179</a>
245
+ <a name='L180'></a><a href='#L180'>180</a>
246
+ <a name='L181'></a><a href='#L181'>181</a>
247
+ <a name='L182'></a><a href='#L182'>182</a>
248
+ <a name='L183'></a><a href='#L183'>183</a>
249
+ <a name='L184'></a><a href='#L184'>184</a>
250
+ <a name='L185'></a><a href='#L185'>185</a>
251
+ <a name='L186'></a><a href='#L186'>186</a>
252
+ <a name='L187'></a><a href='#L187'>187</a>
253
+ <a name='L188'></a><a href='#L188'>188</a>
254
+ <a name='L189'></a><a href='#L189'>189</a>
255
+ <a name='L190'></a><a href='#L190'>190</a>
256
+ <a name='L191'></a><a href='#L191'>191</a>
257
+ <a name='L192'></a><a href='#L192'>192</a>
258
+ <a name='L193'></a><a href='#L193'>193</a>
259
+ <a name='L194'></a><a href='#L194'>194</a>
260
+ <a name='L195'></a><a href='#L195'>195</a>
261
+ <a name='L196'></a><a href='#L196'>196</a>
262
+ <a name='L197'></a><a href='#L197'>197</a>
263
+ <a name='L198'></a><a href='#L198'>198</a>
264
+ <a name='L199'></a><a href='#L199'>199</a>
265
+ <a name='L200'></a><a href='#L200'>200</a>
266
+ <a name='L201'></a><a href='#L201'>201</a>
267
+ <a name='L202'></a><a href='#L202'>202</a>
268
+ <a name='L203'></a><a href='#L203'>203</a>
269
+ <a name='L204'></a><a href='#L204'>204</a>
270
+ <a name='L205'></a><a href='#L205'>205</a>
271
+ <a name='L206'></a><a href='#L206'>206</a>
272
+ <a name='L207'></a><a href='#L207'>207</a>
273
+ <a name='L208'></a><a href='#L208'>208</a>
274
+ <a name='L209'></a><a href='#L209'>209</a>
275
+ <a name='L210'></a><a href='#L210'>210</a>
276
+ <a name='L211'></a><a href='#L211'>211</a>
277
+ <a name='L212'></a><a href='#L212'>212</a>
278
+ <a name='L213'></a><a href='#L213'>213</a>
279
+ <a name='L214'></a><a href='#L214'>214</a>
280
+ <a name='L215'></a><a href='#L215'>215</a>
281
+ <a name='L216'></a><a href='#L216'>216</a>
282
+ <a name='L217'></a><a href='#L217'>217</a>
283
+ <a name='L218'></a><a href='#L218'>218</a>
284
+ <a name='L219'></a><a href='#L219'>219</a>
285
+ <a name='L220'></a><a href='#L220'>220</a>
286
+ <a name='L221'></a><a href='#L221'>221</a>
287
+ <a name='L222'></a><a href='#L222'>222</a>
288
+ <a name='L223'></a><a href='#L223'>223</a>
289
+ <a name='L224'></a><a href='#L224'>224</a>
290
+ <a name='L225'></a><a href='#L225'>225</a>
291
+ <a name='L226'></a><a href='#L226'>226</a>
292
+ <a name='L227'></a><a href='#L227'>227</a>
293
+ <a name='L228'></a><a href='#L228'>228</a>
294
+ <a name='L229'></a><a href='#L229'>229</a>
295
+ <a name='L230'></a><a href='#L230'>230</a>
296
+ <a name='L231'></a><a href='#L231'>231</a>
297
+ <a name='L232'></a><a href='#L232'>232</a>
298
+ <a name='L233'></a><a href='#L233'>233</a>
299
+ <a name='L234'></a><a href='#L234'>234</a>
300
+ <a name='L235'></a><a href='#L235'>235</a>
301
+ <a name='L236'></a><a href='#L236'>236</a>
302
+ <a name='L237'></a><a href='#L237'>237</a>
303
+ <a name='L238'></a><a href='#L238'>238</a>
304
+ <a name='L239'></a><a href='#L239'>239</a>
305
+ <a name='L240'></a><a href='#L240'>240</a>
306
+ <a name='L241'></a><a href='#L241'>241</a>
307
+ <a name='L242'></a><a href='#L242'>242</a>
308
+ <a name='L243'></a><a href='#L243'>243</a>
309
+ <a name='L244'></a><a href='#L244'>244</a>
310
+ <a name='L245'></a><a href='#L245'>245</a>
311
+ <a name='L246'></a><a href='#L246'>246</a>
312
+ <a name='L247'></a><a href='#L247'>247</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
313
+ <span class="cline-any cline-yes">1x</span>
314
+ <span class="cline-any cline-yes">1x</span>
315
+ <span class="cline-any cline-yes">1x</span>
316
+ <span class="cline-any cline-yes">1x</span>
317
+ <span class="cline-any cline-neutral">&nbsp;</span>
318
+ <span class="cline-any cline-yes">1x</span>
319
+ <span class="cline-any cline-neutral">&nbsp;</span>
320
+ <span class="cline-any cline-neutral">&nbsp;</span>
321
+ <span class="cline-any cline-neutral">&nbsp;</span>
322
+ <span class="cline-any cline-neutral">&nbsp;</span>
323
+ <span class="cline-any cline-yes">1x</span>
324
+ <span class="cline-any cline-neutral">&nbsp;</span>
325
+ <span class="cline-any cline-neutral">&nbsp;</span>
326
+ <span class="cline-any cline-neutral">&nbsp;</span>
327
+ <span class="cline-any cline-neutral">&nbsp;</span>
328
+ <span class="cline-any cline-neutral">&nbsp;</span>
329
+ <span class="cline-any cline-neutral">&nbsp;</span>
330
+ <span class="cline-any cline-neutral">&nbsp;</span>
331
+ <span class="cline-any cline-yes">1x</span>
332
+ <span class="cline-any cline-yes">1x</span>
333
+ <span class="cline-any cline-neutral">&nbsp;</span>
334
+ <span class="cline-any cline-yes">1x</span>
335
+ <span class="cline-any cline-neutral">&nbsp;</span>
336
+ <span class="cline-any cline-yes">1x</span>
337
+ <span class="cline-any cline-yes">36x</span>
338
+ <span class="cline-any cline-yes">11x</span>
339
+ <span class="cline-any cline-neutral">&nbsp;</span>
340
+ <span class="cline-any cline-yes">25x</span>
341
+ <span class="cline-any cline-neutral">&nbsp;</span>
342
+ <span class="cline-any cline-neutral">&nbsp;</span>
343
+ <span class="cline-any cline-neutral">&nbsp;</span>
344
+ <span class="cline-any cline-yes">1x</span>
345
+ <span class="cline-any cline-yes">5x</span>
346
+ <span class="cline-any cline-yes">5x</span>
347
+ <span class="cline-any cline-yes">1x</span>
348
+ <span class="cline-any cline-neutral">&nbsp;</span>
349
+ <span class="cline-any cline-yes">5x</span>
350
+ <span class="cline-any cline-neutral">&nbsp;</span>
351
+ <span class="cline-any cline-yes">4x</span>
352
+ <span class="cline-any cline-yes">3x</span>
353
+ <span class="cline-any cline-neutral">&nbsp;</span>
354
+ <span class="cline-any cline-neutral">&nbsp;</span>
355
+ <span class="cline-any cline-yes">1x</span>
356
+ <span class="cline-any cline-neutral">&nbsp;</span>
357
+ <span class="cline-any cline-yes">1x</span>
358
+ <span class="cline-any cline-yes">1x</span>
359
+ <span class="cline-any cline-neutral">&nbsp;</span>
360
+ <span class="cline-any cline-neutral">&nbsp;</span>
361
+ <span class="cline-any cline-neutral">&nbsp;</span>
362
+ <span class="cline-any cline-neutral">&nbsp;</span>
363
+ <span class="cline-any cline-neutral">&nbsp;</span>
364
+ <span class="cline-any cline-neutral">&nbsp;</span>
365
+ <span class="cline-any cline-neutral">&nbsp;</span>
366
+ <span class="cline-any cline-yes">1x</span>
367
+ <span class="cline-any cline-yes">6x</span>
368
+ <span class="cline-any cline-yes">6x</span>
369
+ <span class="cline-any cline-yes">1x</span>
370
+ <span class="cline-any cline-neutral">&nbsp;</span>
371
+ <span class="cline-any cline-neutral">&nbsp;</span>
372
+ <span class="cline-any cline-yes">5x</span>
373
+ <span class="cline-any cline-yes">5x</span>
374
+ <span class="cline-any cline-yes">1x</span>
375
+ <span class="cline-any cline-neutral">&nbsp;</span>
376
+ <span class="cline-any cline-neutral">&nbsp;</span>
377
+ <span class="cline-any cline-yes">4x</span>
378
+ <span class="cline-any cline-yes">1x</span>
379
+ <span class="cline-any cline-neutral">&nbsp;</span>
380
+ <span class="cline-any cline-neutral">&nbsp;</span>
381
+ <span class="cline-any cline-yes">3x</span>
382
+ <span class="cline-any cline-yes">3x</span>
383
+ <span class="cline-any cline-yes">2x</span>
384
+ <span class="cline-any cline-neutral">&nbsp;</span>
385
+ <span class="cline-any cline-yes">1x</span>
386
+ <span class="cline-any cline-neutral">&nbsp;</span>
387
+ <span class="cline-any cline-neutral">&nbsp;</span>
388
+ <span class="cline-any cline-neutral">&nbsp;</span>
389
+ <span class="cline-any cline-neutral">&nbsp;</span>
390
+ <span class="cline-any cline-yes">1x</span>
391
+ <span class="cline-any cline-yes">2x</span>
392
+ <span class="cline-any cline-yes">2x</span>
393
+ <span class="cline-any cline-yes">2x</span>
394
+ <span class="cline-any cline-yes">1x</span>
395
+ <span class="cline-any cline-neutral">&nbsp;</span>
396
+ <span class="cline-any cline-yes">1x</span>
397
+ <span class="cline-any cline-neutral">&nbsp;</span>
398
+ <span class="cline-any cline-neutral">&nbsp;</span>
399
+ <span class="cline-any cline-neutral">&nbsp;</span>
400
+ <span class="cline-any cline-neutral">&nbsp;</span>
401
+ <span class="cline-any cline-yes">1x</span>
402
+ <span class="cline-any cline-yes">4x</span>
403
+ <span class="cline-any cline-yes">4x</span>
404
+ <span class="cline-any cline-yes">4x</span>
405
+ <span class="cline-any cline-neutral">&nbsp;</span>
406
+ <span class="cline-any cline-yes">3x</span>
407
+ <span class="cline-any cline-neutral">&nbsp;</span>
408
+ <span class="cline-any cline-yes">2x</span>
409
+ <span class="cline-any cline-yes">1x</span>
410
+ <span class="cline-any cline-neutral">&nbsp;</span>
411
+ <span class="cline-any cline-neutral">&nbsp;</span>
412
+ <span class="cline-any cline-yes">2x</span>
413
+ <span class="cline-any cline-yes">2x</span>
414
+ <span class="cline-any cline-neutral">&nbsp;</span>
415
+ <span class="cline-any cline-yes">1x</span>
416
+ <span class="cline-any cline-yes">1x</span>
417
+ <span class="cline-any cline-neutral">&nbsp;</span>
418
+ <span class="cline-any cline-neutral">&nbsp;</span>
419
+ <span class="cline-any cline-neutral">&nbsp;</span>
420
+ <span class="cline-any cline-neutral">&nbsp;</span>
421
+ <span class="cline-any cline-yes">1x</span>
422
+ <span class="cline-any cline-yes">6x</span>
423
+ <span class="cline-any cline-yes">6x</span>
424
+ <span class="cline-any cline-neutral">&nbsp;</span>
425
+ <span class="cline-any cline-yes">3x</span>
426
+ <span class="cline-any cline-yes">3x</span>
427
+ <span class="cline-any cline-yes">2x</span>
428
+ <span class="cline-any cline-neutral">&nbsp;</span>
429
+ <span class="cline-any cline-yes">1x</span>
430
+ <span class="cline-any cline-neutral">&nbsp;</span>
431
+ <span class="cline-any cline-neutral">&nbsp;</span>
432
+ <span class="cline-any cline-neutral">&nbsp;</span>
433
+ <span class="cline-any cline-neutral">&nbsp;</span>
434
+ <span class="cline-any cline-yes">1x</span>
435
+ <span class="cline-any cline-yes">3x</span>
436
+ <span class="cline-any cline-yes">3x</span>
437
+ <span class="cline-any cline-neutral">&nbsp;</span>
438
+ <span class="cline-any cline-yes">2x</span>
439
+ <span class="cline-any cline-yes">2x</span>
440
+ <span class="cline-any cline-yes">1x</span>
441
+ <span class="cline-any cline-neutral">&nbsp;</span>
442
+ <span class="cline-any cline-yes">1x</span>
443
+ <span class="cline-any cline-neutral">&nbsp;</span>
444
+ <span class="cline-any cline-neutral">&nbsp;</span>
445
+ <span class="cline-any cline-neutral">&nbsp;</span>
446
+ <span class="cline-any cline-neutral">&nbsp;</span>
447
+ <span class="cline-any cline-yes">1x</span>
448
+ <span class="cline-any cline-yes">5x</span>
449
+ <span class="cline-any cline-yes">5x</span>
450
+ <span class="cline-any cline-neutral">&nbsp;</span>
451
+ <span class="cline-any cline-yes">4x</span>
452
+ <span class="cline-any cline-yes">4x</span>
453
+ <span class="cline-any cline-yes">4x</span>
454
+ <span class="cline-any cline-yes">2x</span>
455
+ <span class="cline-any cline-neutral">&nbsp;</span>
456
+ <span class="cline-any cline-neutral">&nbsp;</span>
457
+ <span class="cline-any cline-yes">2x</span>
458
+ <span class="cline-any cline-yes">1x</span>
459
+ <span class="cline-any cline-neutral">&nbsp;</span>
460
+ <span class="cline-any cline-yes">1x</span>
461
+ <span class="cline-any cline-neutral">&nbsp;</span>
462
+ <span class="cline-any cline-neutral">&nbsp;</span>
463
+ <span class="cline-any cline-neutral">&nbsp;</span>
464
+ <span class="cline-any cline-neutral">&nbsp;</span>
465
+ <span class="cline-any cline-yes">1x</span>
466
+ <span class="cline-any cline-yes">3x</span>
467
+ <span class="cline-any cline-yes">3x</span>
468
+ <span class="cline-any cline-neutral">&nbsp;</span>
469
+ <span class="cline-any cline-yes">2x</span>
470
+ <span class="cline-any cline-yes">2x</span>
471
+ <span class="cline-any cline-yes">1x</span>
472
+ <span class="cline-any cline-neutral">&nbsp;</span>
473
+ <span class="cline-any cline-yes">1x</span>
474
+ <span class="cline-any cline-neutral">&nbsp;</span>
475
+ <span class="cline-any cline-neutral">&nbsp;</span>
476
+ <span class="cline-any cline-neutral">&nbsp;</span>
477
+ <span class="cline-any cline-neutral">&nbsp;</span>
478
+ <span class="cline-any cline-yes">1x</span>
479
+ <span class="cline-any cline-yes">4x</span>
480
+ <span class="cline-any cline-yes">4x</span>
481
+ <span class="cline-any cline-neutral">&nbsp;</span>
482
+ <span class="cline-any cline-yes">3x</span>
483
+ <span class="cline-any cline-yes">3x</span>
484
+ <span class="cline-any cline-yes">3x</span>
485
+ <span class="cline-any cline-yes">2x</span>
486
+ <span class="cline-any cline-yes">1x</span>
487
+ <span class="cline-any cline-neutral">&nbsp;</span>
488
+ <span class="cline-any cline-yes">1x</span>
489
+ <span class="cline-any cline-neutral">&nbsp;</span>
490
+ <span class="cline-any cline-neutral">&nbsp;</span>
491
+ <span class="cline-any cline-neutral">&nbsp;</span>
492
+ <span class="cline-any cline-neutral">&nbsp;</span>
493
+ <span class="cline-any cline-yes">1x</span>
494
+ <span class="cline-any cline-yes">4x</span>
495
+ <span class="cline-any cline-yes">4x</span>
496
+ <span class="cline-any cline-neutral">&nbsp;</span>
497
+ <span class="cline-any cline-yes">3x</span>
498
+ <span class="cline-any cline-yes">3x</span>
499
+ <span class="cline-any cline-yes">3x</span>
500
+ <span class="cline-any cline-yes">2x</span>
501
+ <span class="cline-any cline-neutral">&nbsp;</span>
502
+ <span class="cline-any cline-yes">1x</span>
503
+ <span class="cline-any cline-neutral">&nbsp;</span>
504
+ <span class="cline-any cline-neutral">&nbsp;</span>
505
+ <span class="cline-any cline-neutral">&nbsp;</span>
506
+ <span class="cline-any cline-neutral">&nbsp;</span>
507
+ <span class="cline-any cline-yes">1x</span>
508
+ <span class="cline-any cline-yes">3x</span>
509
+ <span class="cline-any cline-yes">3x</span>
510
+ <span class="cline-any cline-neutral">&nbsp;</span>
511
+ <span class="cline-any cline-yes">2x</span>
512
+ <span class="cline-any cline-yes">2x</span>
513
+ <span class="cline-any cline-yes">2x</span>
514
+ <span class="cline-any cline-yes">1x</span>
515
+ <span class="cline-any cline-neutral">&nbsp;</span>
516
+ <span class="cline-any cline-yes">1x</span>
517
+ <span class="cline-any cline-neutral">&nbsp;</span>
518
+ <span class="cline-any cline-neutral">&nbsp;</span>
519
+ <span class="cline-any cline-neutral">&nbsp;</span>
520
+ <span class="cline-any cline-neutral">&nbsp;</span>
521
+ <span class="cline-any cline-yes">1x</span>
522
+ <span class="cline-any cline-yes">4x</span>
523
+ <span class="cline-any cline-yes">4x</span>
524
+ <span class="cline-any cline-neutral">&nbsp;</span>
525
+ <span class="cline-any cline-yes">3x</span>
526
+ <span class="cline-any cline-yes">3x</span>
527
+ <span class="cline-any cline-yes">1x</span>
528
+ <span class="cline-any cline-neutral">&nbsp;</span>
529
+ <span class="cline-any cline-neutral">&nbsp;</span>
530
+ <span class="cline-any cline-yes">2x</span>
531
+ <span class="cline-any cline-neutral">&nbsp;</span>
532
+ <span class="cline-any cline-neutral">&nbsp;</span>
533
+ <span class="cline-any cline-neutral">&nbsp;</span>
534
+ <span class="cline-any cline-neutral">&nbsp;</span>
535
+ <span class="cline-any cline-neutral">&nbsp;</span>
536
+ <span class="cline-any cline-yes">1x</span>
537
+ <span class="cline-any cline-neutral">&nbsp;</span>
538
+ <span class="cline-any cline-yes">1x</span>
539
+ <span class="cline-any cline-neutral">&nbsp;</span>
540
+ <span class="cline-any cline-neutral">&nbsp;</span>
541
+ <span class="cline-any cline-neutral">&nbsp;</span>
542
+ <span class="cline-any cline-neutral">&nbsp;</span>
543
+ <span class="cline-any cline-yes">1x</span>
544
+ <span class="cline-any cline-yes">4x</span>
545
+ <span class="cline-any cline-yes">4x</span>
546
+ <span class="cline-any cline-neutral">&nbsp;</span>
547
+ <span class="cline-any cline-yes">3x</span>
548
+ <span class="cline-any cline-yes">3x</span>
549
+ <span class="cline-any cline-yes">3x</span>
550
+ <span class="cline-any cline-yes">2x</span>
551
+ <span class="cline-any cline-yes">1x</span>
552
+ <span class="cline-any cline-neutral">&nbsp;</span>
553
+ <span class="cline-any cline-yes">1x</span>
554
+ <span class="cline-any cline-neutral">&nbsp;</span>
555
+ <span class="cline-any cline-neutral">&nbsp;</span>
556
+ <span class="cline-any cline-neutral">&nbsp;</span>
557
+ <span class="cline-any cline-yes">1x</span>
558
+ <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">const express = require('express');
559
+ const multer = require('multer');
560
+ const rateLimit = require('express-rate-limit');
561
+ const router = express.Router();
562
+ const jiraService = require('../service/jiraService');
563
+ &nbsp;
564
+ const upload = multer({
565
+ storage: multer.memoryStorage(),
566
+ limits: { fileSize: 10 * 1024 * 1024 }
567
+ });
568
+ &nbsp;
569
+ const mutationLimiter = rateLimit({
570
+ windowMs: 60 * 1000,
571
+ max: 10,
572
+ standardHeaders: true,
573
+ legacyHeaders: false,
574
+ message: { error: 'Too many requests, please slow down.' },
575
+ });
576
+ &nbsp;
577
+ const ISSUE_KEY_REGEX = /^[A-Z]+-\d+$/;
578
+ const DANGEROUS_CHARS = /['";\\-]|--/g;
579
+ &nbsp;
580
+ const escapeJql = <span class="fstat-no" title="function not covered" >(s</span>tr) =&gt; <span class="cstat-no" title="statement not covered" >String(str).replace(/'/g, "\\'");</span>
581
+ &nbsp;
582
+ const validateIssueKey = (key) =&gt; {
583
+ if (!ISSUE_KEY_REGEX.test(key)) {
584
+ return 'Invalid issue key format. Expected format: PROJECT-123';
585
+ }
586
+ return null;
587
+ };
588
+ &nbsp;
589
+ // GET /api/issues
590
+ router.get('/', async (req, res) =&gt; {
591
+ try {
592
+ if (req.query.search) {
593
+ req.query.search = String(req.query.search).replace(DANGEROUS_CHARS, '');
594
+ }
595
+ const data = await jiraService.searchIssues(req.query);
596
+
597
+ if (!data || !data.issues || data.issues.length === 0) {
598
+ return res.status(200).json({ issues: [], total: 0, message: 'No issues found' });
599
+ }
600
+ &nbsp;
601
+ res.json(data);
602
+ } catch (error) {
603
+ console.error('Error fetching issues:', error.message);
604
+ res.status(500).json({
605
+ error: 'Failed to fetch issues',
606
+ details: error.message
607
+ });
608
+ }
609
+ });
610
+ &nbsp;
611
+ // POST /api/issues
612
+ router.post('/', mutationLimiter, async (req, res) =&gt; {
613
+ try {
614
+ if (!req.body.summary || typeof req.body.summary !== 'string') {
615
+ return res.status(400).json({ error: 'Summary is required' });
616
+ }
617
+ &nbsp;
618
+ const summary = req.body.summary.trim();
619
+ if (summary.length === 0) {
620
+ return res.status(400).json({ error: 'Summary cannot be empty' });
621
+ }
622
+ &nbsp;
623
+ if (summary.length &gt; 255) {
624
+ return res.status(400).json({ error: 'Summary must be under 255 characters' });
625
+ }
626
+ &nbsp;
627
+ req.body.summary = summary;
628
+ const newIssue = await jiraService.createIssue(req.body);
629
+ res.status(201).json(newIssue);
630
+ } catch (error) {
631
+ res.status(500).json({ error: 'Failed to create issue', details: error.message });
632
+ }
633
+ });
634
+ &nbsp;
635
+ // GET /api/issues/labels
636
+ router.get('/labels/search', async (req, res) =&gt; {
637
+ try {
638
+ const { query } = req.query;
639
+ const labels = await jiraService.searchLabels(query);
640
+ res.json(labels);
641
+ } catch (error) {
642
+ res.status(500).json({ error: 'Failed to search labels', details: error.message });
643
+ }
644
+ });
645
+ &nbsp;
646
+ // GET /api/issues/image/proxy
647
+ router.get('/image/proxy', async (req, res) =&gt; {
648
+ try {
649
+ const { url } = req.query;
650
+ if (!url) return res.status(400).send('URL is required');
651
+ &nbsp;
652
+ const remoteResponse = await jiraService.downloadAttachment(url);
653
+ // Relay headers (content-type)
654
+ if (remoteResponse.headers['content-type']) {
655
+ res.set('Content-Type', remoteResponse.headers['content-type']);
656
+ }
657
+ // Browser-only cache for 24 hours
658
+ res.set('Cache-Control', 'public, max-age=86400');
659
+ remoteResponse.data.pipe(res);
660
+ } catch (error) {
661
+ console.error('Error proxying image:', error.message);
662
+ res.status(500).send('Failed to fetch image');
663
+ }
664
+ });
665
+ &nbsp;
666
+ // GET /api/issues/:issueKey
667
+ router.get('/:issueKey', async (req, res) =&gt; {
668
+ const error = validateIssueKey(req.params.issueKey);
669
+ if (error) return res.status(400).json({ error });
670
+
671
+ try {
672
+ const issue = await jiraService.getIssueDetail(req.params.issueKey);
673
+ res.json(issue);
674
+ } catch (error) {
675
+ res.status(500).json({ error: 'Failed to fetch issue details', details: error.message });
676
+ }
677
+ });
678
+ &nbsp;
679
+ // GET /api/issues/:issueKey/comments
680
+ router.get('/:issueKey/comments', async (req, res) =&gt; {
681
+ const error = validateIssueKey(req.params.issueKey);
682
+ if (error) return res.status(400).json({ error });
683
+
684
+ try {
685
+ const comments = await jiraService.getComments(req.params.issueKey);
686
+ res.json(comments);
687
+ } catch (error) {
688
+ res.status(500).json({ error: 'Failed to fetch comments', details: error.message });
689
+ }
690
+ });
691
+ &nbsp;
692
+ // POST /api/issues/:issueKey/comments
693
+ router.post('/:issueKey/comments', mutationLimiter, async (req, res) =&gt; {
694
+ const error = validateIssueKey(req.params.issueKey);
695
+ if (error) return res.status(400).json({ error });
696
+
697
+ try {
698
+ const { body } = req.body;
699
+ if (!body || !body.trim()) {
700
+ return res.status(400).json({ error: 'Comment body cannot be empty' });
701
+ }
702
+
703
+ const comment = await jiraService.addComment(req.params.issueKey, body);
704
+ res.status(201).json(comment);
705
+ } catch (error) {
706
+ res.status(500).json({ error: 'Failed to post comment', details: error.message });
707
+ }
708
+ });
709
+ &nbsp;
710
+ // GET /api/issues/:issueKey/transitions
711
+ router.get('/:issueKey/transitions', async (req, res) =&gt; {
712
+ const error = validateIssueKey(req.params.issueKey);
713
+ if (error) return res.status(400).json({ error });
714
+
715
+ try {
716
+ const transitions = await jiraService.getTransitions(req.params.issueKey);
717
+ res.json(transitions);
718
+ } catch (error) {
719
+ res.status(500).json({ error: 'Failed to fetch transitions', details: error.message });
720
+ }
721
+ });
722
+ &nbsp;
723
+ // POST /api/issues/:issueKey/transitions
724
+ router.post('/:issueKey/transitions', mutationLimiter, async (req, res) =&gt; {
725
+ const error = validateIssueKey(req.params.issueKey);
726
+ if (error) return res.status(400).json({ error });
727
+
728
+ try {
729
+ const { transitionId } = req.body;
730
+ if (!transitionId) return res.status(400).json({ error: 'transitionId is required' });
731
+ await jiraService.transitionIssue(req.params.issueKey, transitionId);
732
+ res.json({ success: true });
733
+ } catch (error) {
734
+ res.status(500).json({ error: 'Failed to transition issue', details: error.message });
735
+ }
736
+ });
737
+ &nbsp;
738
+ // GET /api/issues/:issueKey/assignable
739
+ router.get('/:issueKey/assignable', async (req, res) =&gt; {
740
+ const error = validateIssueKey(req.params.issueKey);
741
+ if (error) return res.status(400).json({ error });
742
+
743
+ try {
744
+ const { query } = req.query;
745
+ const users = await jiraService.getAssignableUsers(req.params.issueKey, query);
746
+ res.json(users);
747
+ } catch (error) {
748
+ res.status(500).json({ error: 'Failed to fetch assignable users', details: error.message });
749
+ }
750
+ });
751
+ &nbsp;
752
+ // PUT /api/issues/:issueKey/assignee
753
+ router.put('/:issueKey/assignee', mutationLimiter, async (req, res) =&gt; {
754
+ const error = validateIssueKey(req.params.issueKey);
755
+ if (error) return res.status(400).json({ error });
756
+
757
+ try {
758
+ const { accountId } = req.body;
759
+ await jiraService.assignIssue(req.params.issueKey, accountId);
760
+ res.json({ success: true });
761
+ } catch (error) {
762
+ res.status(500).json({ error: 'Failed to assign issue', details: error.message });
763
+ }
764
+ });
765
+ &nbsp;
766
+ // POST /api/issues/:issueKey/attachments
767
+ router.post('/:issueKey/attachments', mutationLimiter, upload.single('file'), async (req, res) =&gt; {
768
+ const error = validateIssueKey(req.params.issueKey);
769
+ if (error) return res.status(400).json({ error });
770
+
771
+ try {
772
+ if (!req.file) {
773
+ return res.status(400).json({ error: 'No file uploaded' });
774
+ }
775
+
776
+ const attachments = await jiraService.attachFile(
777
+ req.params.issueKey,
778
+ req.file.buffer,
779
+ req.file.originalname,
780
+ req.file.mimetype
781
+ );
782
+ res.status(201).json(attachments);
783
+ } catch (error) {
784
+ res.status(500).json({ error: 'Failed to upload attachment', details: error.message });
785
+ }
786
+ });
787
+ &nbsp;
788
+ // PUT /api/issues/:issueKey
789
+ router.put('/:issueKey', mutationLimiter, async (req, res) =&gt; {
790
+ const error = validateIssueKey(req.params.issueKey);
791
+ if (error) return res.status(400).json({ error });
792
+
793
+ try {
794
+ const { fields } = req.body;
795
+ if (!fields) return res.status(400).json({ error: 'fields are required' });
796
+ await jiraService.updateIssue(req.params.issueKey, fields);
797
+ res.json({ success: true });
798
+ } catch (error) {
799
+ res.status(500).json({ error: 'Failed to update issue', details: error.message });
800
+ }
801
+ });
802
+ &nbsp;
803
+ module.exports = router;
804
+ &nbsp;</pre></td></tr></table></pre>
805
+
806
+ <div class='push'></div><!-- for sticky footer -->
807
+ </div><!-- /wrapper -->
808
+ <div class='footer quiet pad2 space-top1 center small'>
809
+ Code coverage generated by
810
+ <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
811
+ at 2026-03-22T01:07:41.164Z
812
+ </div>
813
+ <script src="../prettify.js"></script>
814
+ <script>
815
+ window.onload = function () {
816
+ prettyPrint();
817
+ };
818
+ </script>
819
+ <script src="../sorter.js"></script>
820
+ <script src="../block-navigation.js"></script>
821
+ </body>
822
+ </html>
823
+