buddy-workbench 0.1.12 → 0.1.14
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/package.json +1 -1
- package/server/config.js +1 -0
- package/server/repositories/postman.js +31 -0
- package/server/repositories/settings.js +21 -1
- package/server/routes/bookmark-sync.js +42 -0
- package/server/routes/postman.js +283 -0
- package/server/routes/settings.js +27 -2
- package/server/services/bookmark-sync.js +435 -0
- package/server/services/browser.js +70 -0
- package/server/services/clipboard-history.js +222 -44
- package/server/services/dialog.js +43 -0
- package/server/services/postman-parser.js +300 -0
- package/server.js +4 -0
- package/ui/dist/assets/{index-BuBK4DeN.css → index-B9y46rtV.css} +1 -1
- package/ui/dist/assets/index-MhpHbNrk.js +524 -0
- package/ui/dist/index.html +10 -4
- package/ui/dist/assets/index-BfTsErqX.js +0 -521
package/package.json
CHANGED
package/server/config.js
CHANGED
|
@@ -11,6 +11,7 @@ export const paths = {
|
|
|
11
11
|
todos: join(root, 'data', 'todos.json'),
|
|
12
12
|
staticPages: join(root, 'data', 'static-pages.json'),
|
|
13
13
|
errors: join(root, 'data', 'errors.json'),
|
|
14
|
+
postman: join(root, 'data', 'postman.json'),
|
|
14
15
|
shutdownLog: join(root, 'data', 'shutdown.log'),
|
|
15
16
|
clipboardDir: join(root, 'data', 'clipboard'),
|
|
16
17
|
plugins: join(root, 'plugins'),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { paths } from '../config.js';
|
|
4
|
+
|
|
5
|
+
const defaultData = {
|
|
6
|
+
collections: [],
|
|
7
|
+
environments: [],
|
|
8
|
+
activeEnvironmentId: null
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function getPostmanData() {
|
|
12
|
+
try {
|
|
13
|
+
if (!existsSync(paths.postman)) {
|
|
14
|
+
return defaultData;
|
|
15
|
+
}
|
|
16
|
+
const raw = readFileSync(paths.postman, 'utf8');
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
return {
|
|
19
|
+
collections: parsed.collections || [],
|
|
20
|
+
environments: parsed.environments || [],
|
|
21
|
+
activeEnvironmentId: parsed.activeEnvironmentId || null
|
|
22
|
+
};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return defaultData;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function savePostmanData(data) {
|
|
29
|
+
mkdirSync(dirname(paths.postman), { recursive: true });
|
|
30
|
+
writeFileSync(paths.postman, JSON.stringify(data, null, 2), 'utf8');
|
|
31
|
+
}
|
|
@@ -17,7 +17,10 @@ export function settingsStatus() {
|
|
|
17
17
|
bitbucketTokenConfigured: Boolean(settings.bitbucketAccessToken),
|
|
18
18
|
jiraTokenConfigured: Boolean(settings.jiraAccessToken),
|
|
19
19
|
confluenceTokenConfigured: Boolean(settings.confluenceAccessToken),
|
|
20
|
-
|
|
20
|
+
isMac: process.platform === 'darwin',
|
|
21
|
+
clipboardEnabled: settings.clipboardEnabled !== false,
|
|
22
|
+
clipboardImageEnabled: settings.clipboardImageEnabled !== false,
|
|
23
|
+
clipboardDeduplicateMinutes: typeof settings.clipboardDeduplicateMinutes === 'number' && !isNaN(settings.clipboardDeduplicateMinutes) ? settings.clipboardDeduplicateMinutes : 60
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -65,6 +68,23 @@ export function saveClipboardEnabled(enabled) {
|
|
|
65
68
|
return settingsStatus();
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
export function saveClipboardImageEnabled(enabled) {
|
|
72
|
+
const settings = readSettings();
|
|
73
|
+
settings.clipboardImageEnabled = Boolean(enabled);
|
|
74
|
+
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
75
|
+
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
76
|
+
return settingsStatus();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function saveClipboardDeduplicateMinutes(minutes) {
|
|
80
|
+
const settings = readSettings();
|
|
81
|
+
const val = Number(minutes);
|
|
82
|
+
settings.clipboardDeduplicateMinutes = !isNaN(val) && val >= 0 ? val : 60;
|
|
83
|
+
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
84
|
+
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
85
|
+
return settingsStatus();
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
const accessTokenFields = {
|
|
69
89
|
bitbucket: 'bitbucketAccessToken',
|
|
70
90
|
jira: 'jiraAccessToken',
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import {
|
|
3
|
+
getBookmarkSyncStatus,
|
|
4
|
+
previewSync,
|
|
5
|
+
performSync
|
|
6
|
+
} from '../services/bookmark-sync.js';
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
// Get status of Chrome & Edge bookmark files & processes
|
|
11
|
+
router.get('/status', (req, res, next) => {
|
|
12
|
+
try {
|
|
13
|
+
const status = getBookmarkSyncStatus();
|
|
14
|
+
res.json(status);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
next(error);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Preview sync changes
|
|
21
|
+
router.get('/preview', (req, res, next) => {
|
|
22
|
+
try {
|
|
23
|
+
const mode = req.query.mode || 'two-way';
|
|
24
|
+
const preview = previewSync(mode);
|
|
25
|
+
res.json(preview);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
next(error);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Perform actual sync
|
|
32
|
+
router.post('/sync', (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const mode = req.body?.mode || 'two-way';
|
|
35
|
+
const result = performSync(mode);
|
|
36
|
+
res.json(result);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
next(error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export default router;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { getPostmanData, savePostmanData } from '../repositories/postman.js';
|
|
4
|
+
import { parsePostmanCollection } from '../services/postman-parser.js';
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
// Variable substitution helper
|
|
9
|
+
function replaceVariables(str, varMap) {
|
|
10
|
+
if (typeof str !== 'string') return str;
|
|
11
|
+
return str.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (match, key) => {
|
|
12
|
+
return varMap[key] !== undefined ? varMap[key] : match;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// GET all Postman workbench data
|
|
17
|
+
router.get('/', (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const data = getPostmanData();
|
|
20
|
+
res.json(data);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
res.status(500).json({ error: error.message || 'Failed to fetch Postman data' });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Import collection
|
|
27
|
+
router.post('/import', (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const { collectionJson } = req.body;
|
|
30
|
+
if (!collectionJson) {
|
|
31
|
+
return res.status(400).json({ error: 'Collection JSON content is required' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parsedCollection = parsePostmanCollection(collectionJson);
|
|
35
|
+
const data = getPostmanData();
|
|
36
|
+
|
|
37
|
+
// Check if collection with same ID or name exists
|
|
38
|
+
const existingIndex = data.collections.findIndex((c) => c.id === parsedCollection.id);
|
|
39
|
+
if (existingIndex >= 0) {
|
|
40
|
+
data.collections[existingIndex] = parsedCollection;
|
|
41
|
+
} else {
|
|
42
|
+
data.collections.unshift(parsedCollection);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
savePostmanData(data);
|
|
46
|
+
res.status(201).json(parsedCollection);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
res.status(400).json({ error: `Invalid Postman collection JSON: ${error.message}` });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Create collection manually
|
|
53
|
+
router.post('/collections', (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const { name, description } = req.body;
|
|
56
|
+
if (!name) {
|
|
57
|
+
return res.status(400).json({ error: 'Collection name is required' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = getPostmanData();
|
|
61
|
+
const newCollection = {
|
|
62
|
+
id: `col_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
63
|
+
name,
|
|
64
|
+
description: description || '',
|
|
65
|
+
variables: [],
|
|
66
|
+
items: [],
|
|
67
|
+
createdAt: new Date().toISOString()
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
data.collections.unshift(newCollection);
|
|
71
|
+
savePostmanData(data);
|
|
72
|
+
res.status(201).json(newCollection);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
res.status(500).json({ error: error.message || 'Failed to create collection' });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Update collection
|
|
79
|
+
router.put('/collections/:id', (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const { id } = req.params;
|
|
82
|
+
const { name, description, variables, items } = req.body;
|
|
83
|
+
|
|
84
|
+
const data = getPostmanData();
|
|
85
|
+
const index = data.collections.findIndex((c) => c.id === id);
|
|
86
|
+
if (index === -1) {
|
|
87
|
+
return res.status(404).json({ error: 'Collection not found' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const updated = {
|
|
91
|
+
...data.collections[index],
|
|
92
|
+
...(name !== undefined && { name }),
|
|
93
|
+
...(description !== undefined && { description }),
|
|
94
|
+
...(variables !== undefined && { variables }),
|
|
95
|
+
...(items !== undefined && { items }),
|
|
96
|
+
updatedAt: new Date().toISOString()
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
data.collections[index] = updated;
|
|
100
|
+
savePostmanData(data);
|
|
101
|
+
res.json(updated);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
res.status(500).json({ error: error.message || 'Failed to update collection' });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Delete collection
|
|
108
|
+
router.delete('/collections/:id', (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const { id } = req.params;
|
|
111
|
+
const data = getPostmanData();
|
|
112
|
+
data.collections = data.collections.filter((c) => c.id !== id);
|
|
113
|
+
savePostmanData(data);
|
|
114
|
+
res.json({ success: true });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
res.status(500).json({ error: error.message || 'Failed to delete collection' });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Save environments
|
|
121
|
+
router.post('/environments', (req, res) => {
|
|
122
|
+
try {
|
|
123
|
+
const { environments, activeEnvironmentId } = req.body;
|
|
124
|
+
const data = getPostmanData();
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(environments)) {
|
|
127
|
+
data.environments = environments;
|
|
128
|
+
}
|
|
129
|
+
if (activeEnvironmentId !== undefined) {
|
|
130
|
+
data.activeEnvironmentId = activeEnvironmentId;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
savePostmanData(data);
|
|
134
|
+
res.json({
|
|
135
|
+
environments: data.environments,
|
|
136
|
+
activeEnvironmentId: data.activeEnvironmentId
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
res.status(500).json({ error: error.message || 'Failed to save environments' });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// HTTP Request Proxy execution
|
|
144
|
+
router.post('/send', async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const {
|
|
147
|
+
method = 'GET',
|
|
148
|
+
url = '',
|
|
149
|
+
params = [],
|
|
150
|
+
headers = [],
|
|
151
|
+
body = {},
|
|
152
|
+
auth = {},
|
|
153
|
+
environmentVariables = [],
|
|
154
|
+
collectionVariables = []
|
|
155
|
+
} = req.body;
|
|
156
|
+
|
|
157
|
+
if (!url) {
|
|
158
|
+
return res.status(400).json({ error: 'Target URL is required' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Build variable mapping (environment overrides collection)
|
|
162
|
+
const varMap = {};
|
|
163
|
+
if (Array.isArray(collectionVariables)) {
|
|
164
|
+
collectionVariables.forEach((v) => {
|
|
165
|
+
if (v.enabled !== false && v.key) varMap[v.key] = v.value || '';
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(environmentVariables)) {
|
|
169
|
+
environmentVariables.forEach((v) => {
|
|
170
|
+
if (v.enabled !== false && v.key) varMap[v.key] = v.value || '';
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Replace variables in URL
|
|
175
|
+
let resolvedUrl = replaceVariables(url, varMap);
|
|
176
|
+
if (!resolvedUrl.startsWith('http://') && !resolvedUrl.startsWith('https://')) {
|
|
177
|
+
resolvedUrl = `http://${resolvedUrl}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Process Params
|
|
181
|
+
const queryObj = {};
|
|
182
|
+
if (Array.isArray(params)) {
|
|
183
|
+
params.forEach((p) => {
|
|
184
|
+
if (p.enabled !== false && p.key) {
|
|
185
|
+
queryObj[replaceVariables(p.key, varMap)] = replaceVariables(p.value || '', varMap);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Process Headers
|
|
191
|
+
const reqHeaders = {};
|
|
192
|
+
if (Array.isArray(headers)) {
|
|
193
|
+
headers.forEach((h) => {
|
|
194
|
+
if (h.enabled !== false && h.key) {
|
|
195
|
+
reqHeaders[replaceVariables(h.key, varMap)] = replaceVariables(h.value || '', varMap);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Process Auth
|
|
201
|
+
if (auth && auth.type === 'bearer' && auth.token) {
|
|
202
|
+
reqHeaders['Authorization'] = `Bearer ${replaceVariables(auth.token, varMap)}`;
|
|
203
|
+
} else if (auth && auth.type === 'basic' && (auth.username || auth.password)) {
|
|
204
|
+
const u = replaceVariables(auth.username || '', varMap);
|
|
205
|
+
const p = replaceVariables(auth.password || '', varMap);
|
|
206
|
+
const credentials = Buffer.from(`${u}:${p}`).toString('base64');
|
|
207
|
+
reqHeaders['Authorization'] = `Basic ${credentials}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Process Body
|
|
211
|
+
let reqData = undefined;
|
|
212
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase())) {
|
|
213
|
+
if (body.mode === 'raw' || body.mode === 'json') {
|
|
214
|
+
const rawContent = replaceVariables(body.raw || body.json || '', varMap);
|
|
215
|
+
if (rawContent) {
|
|
216
|
+
reqData = rawContent;
|
|
217
|
+
if (!Object.keys(reqHeaders).some((k) => k.toLowerCase() === 'content-type')) {
|
|
218
|
+
reqHeaders['Content-Type'] = 'application/json';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else if (body.mode === 'urlencoded') {
|
|
222
|
+
const urlParams = new URLSearchParams();
|
|
223
|
+
if (Array.isArray(body.urlencoded)) {
|
|
224
|
+
body.urlencoded.forEach((u) => {
|
|
225
|
+
if (u.enabled !== false && u.key) {
|
|
226
|
+
urlParams.append(replaceVariables(u.key, varMap), replaceVariables(u.value || '', varMap));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
reqData = urlParams.toString();
|
|
231
|
+
if (!Object.keys(reqHeaders).some((k) => k.toLowerCase() === 'content-type')) {
|
|
232
|
+
reqHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const startTime = Date.now();
|
|
238
|
+
try {
|
|
239
|
+
const response = await axios({
|
|
240
|
+
method,
|
|
241
|
+
url: resolvedUrl,
|
|
242
|
+
params: Object.keys(queryObj).length > 0 ? queryObj : undefined,
|
|
243
|
+
headers: reqHeaders,
|
|
244
|
+
data: reqData,
|
|
245
|
+
validateStatus: () => true, // Don't throw on non-2xx status codes
|
|
246
|
+
timeout: 30000,
|
|
247
|
+
maxContentLength: 10 * 1024 * 1024 // 10MB limit
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const durationMs = Date.now() - startTime;
|
|
251
|
+
let responseBody = response.data;
|
|
252
|
+
const responseHeaders = response.headers || {};
|
|
253
|
+
const contentType = responseHeaders['content-type'] || responseHeaders['Content-Type'] || '';
|
|
254
|
+
|
|
255
|
+
const bodyString = typeof responseBody === 'string'
|
|
256
|
+
? responseBody
|
|
257
|
+
: JSON.stringify(responseBody || '');
|
|
258
|
+
const sizeBytes = Buffer.byteLength(bodyString, 'utf8');
|
|
259
|
+
|
|
260
|
+
res.json({
|
|
261
|
+
status: response.status,
|
|
262
|
+
statusText: response.statusText,
|
|
263
|
+
headers: responseHeaders,
|
|
264
|
+
data: responseBody,
|
|
265
|
+
durationMs,
|
|
266
|
+
sizeBytes,
|
|
267
|
+
contentType,
|
|
268
|
+
resolvedUrl
|
|
269
|
+
});
|
|
270
|
+
} catch (reqErr) {
|
|
271
|
+
const durationMs = Date.now() - startTime;
|
|
272
|
+
res.status(502).json({
|
|
273
|
+
error: reqErr.message || 'Request execution failed',
|
|
274
|
+
resolvedUrl,
|
|
275
|
+
durationMs
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
res.status(500).json({ error: error.message || 'Failed to send request' });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export default router;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { saveAccessToken, saveClipboardEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, settingsStatus } from '../repositories/settings.js';
|
|
3
|
-
import { openInSystemBrowser } from '../services/browser.js';
|
|
2
|
+
import { saveAccessToken, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, settingsStatus } from '../repositories/settings.js';
|
|
3
|
+
import { openInSystemBrowser, openInSystemEditor } from '../services/browser.js';
|
|
4
|
+
import { selectDirectory } from '../services/dialog.js';
|
|
4
5
|
|
|
5
6
|
const router = Router();
|
|
6
7
|
|
|
@@ -31,11 +32,35 @@ router.post('/open-url', (req, res) => {
|
|
|
31
32
|
openInSystemBrowser(url.trim());
|
|
32
33
|
res.json({ success: true });
|
|
33
34
|
});
|
|
35
|
+
router.post('/open-editor', (req, res) => {
|
|
36
|
+
const { path, location } = req.body || {};
|
|
37
|
+
if (typeof path !== 'string' || !path) return res.status(400).json({ error: 'Path must be a non-empty string.' });
|
|
38
|
+
openInSystemEditor(path.trim(), location ? String(location).trim() : '');
|
|
39
|
+
res.json({ success: true });
|
|
40
|
+
});
|
|
41
|
+
router.post('/select-directory', async (_req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const result = await selectDirectory();
|
|
44
|
+
res.json(result);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
res.status(500).json({ error: error.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
34
49
|
router.put('/clipboard-enabled', (req, res) => {
|
|
35
50
|
const { enabled } = req.body || {};
|
|
36
51
|
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
|
|
37
52
|
res.json(saveClipboardEnabled(enabled));
|
|
38
53
|
});
|
|
54
|
+
router.put('/clipboard-image-enabled', (req, res) => {
|
|
55
|
+
const { enabled } = req.body || {};
|
|
56
|
+
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
|
|
57
|
+
res.json(saveClipboardImageEnabled(enabled));
|
|
58
|
+
});
|
|
59
|
+
router.put('/clipboard-deduplicate-minutes', (req, res) => {
|
|
60
|
+
const { minutes } = req.body || {};
|
|
61
|
+
if (typeof minutes !== 'number' || isNaN(minutes) || minutes < 0) return res.status(400).json({ error: 'Minutes must be a non-negative number.' });
|
|
62
|
+
res.json(saveClipboardDeduplicateMinutes(minutes));
|
|
63
|
+
});
|
|
39
64
|
router.put('/:service-access-token', (req, res) => {
|
|
40
65
|
const { token } = req.body || {};
|
|
41
66
|
if (typeof token !== 'string') return res.status(400).json({ error: 'Token must be a string.' });
|