fa-mcp-sdk 0.4.3 → 0.4.6
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/bin/fa-mcp.js +1040 -1039
- package/cli-template/eslint.config.js +16 -136
- package/cli-template/package.json +9 -10
- package/cli-template/tsconfig.json +1 -0
- package/dist/core/_types_/active-directory-config.d.ts.map +1 -1
- package/dist/core/_types_/config.d.ts +1 -1
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/ad/group-checker.d.ts.map +1 -1
- package/dist/core/ad/group-checker.js.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.js +8 -8
- package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
- package/dist/core/agent-tester/check-llm.d.ts.map +1 -1
- package/dist/core/agent-tester/check-llm.js +1 -1
- package/dist/core/agent-tester/check-llm.js.map +1 -1
- package/dist/core/agent-tester/services/TesterAgentService.d.ts.map +1 -1
- package/dist/core/agent-tester/services/TesterAgentService.js +53 -53
- package/dist/core/agent-tester/services/TesterAgentService.js.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.js +2 -2
- package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
- package/dist/core/auth/admin-auth.d.ts.map +1 -1
- package/dist/core/auth/admin-auth.js +3 -3
- package/dist/core/auth/admin-auth.js.map +1 -1
- package/dist/core/auth/basic.d.ts.map +1 -1
- package/dist/core/auth/basic.js.map +1 -1
- package/dist/core/auth/jwt.d.ts.map +1 -1
- package/dist/core/auth/jwt.js +6 -16
- package/dist/core/auth/jwt.js.map +1 -1
- package/dist/core/auth/middleware.d.ts.map +1 -1
- package/dist/core/auth/middleware.js +3 -2
- package/dist/core/auth/middleware.js.map +1 -1
- package/dist/core/auth/multi-auth.d.ts +0 -3
- package/dist/core/auth/multi-auth.d.ts.map +1 -1
- package/dist/core/auth/multi-auth.js +10 -7
- package/dist/core/auth/multi-auth.js.map +1 -1
- package/dist/core/auth/permanent.d.ts.map +1 -1
- package/dist/core/auth/permanent.js +1 -1
- package/dist/core/auth/permanent.js.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.d.ts.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js +2 -2
- package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-integration.d.ts.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-integration.js +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-integration.js.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-templates.d.ts.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-templates.js +222 -221
- package/dist/core/auth/token-generator/ntlm/ntlm-templates.js.map +1 -1
- package/dist/core/auth/token-generator/server.d.ts.map +1 -1
- package/dist/core/auth/token-generator/server.js +8 -8
- package/dist/core/auth/token-generator/server.js.map +1 -1
- package/dist/core/bootstrap/init-config.d.ts.map +1 -1
- package/dist/core/bootstrap/init-config.js +4 -4
- package/dist/core/bootstrap/init-config.js.map +1 -1
- package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
- package/dist/core/bootstrap/startup-info.js +4 -4
- package/dist/core/bootstrap/startup-info.js.map +1 -1
- package/dist/core/cache/cache.d.ts.map +1 -1
- package/dist/core/cache/cache.js +3 -3
- package/dist/core/cache/cache.js.map +1 -1
- package/dist/core/consul/access-points-updater.d.ts.map +1 -1
- package/dist/core/consul/access-points-updater.js +3 -3
- package/dist/core/consul/access-points-updater.js.map +1 -1
- package/dist/core/consul/deregister.d.ts.map +1 -1
- package/dist/core/consul/deregister.js +1 -1
- package/dist/core/consul/deregister.js.map +1 -1
- package/dist/core/consul/get-consul-api.d.ts.map +1 -1
- package/dist/core/consul/get-consul-api.js +3 -3
- package/dist/core/consul/get-consul-api.js.map +1 -1
- package/dist/core/db/pg-db.d.ts +1 -1
- package/dist/core/db/pg-db.d.ts.map +1 -1
- package/dist/core/db/pg-db.js +2 -2
- package/dist/core/db/pg-db.js.map +1 -1
- package/dist/core/debug.js +1 -1
- package/dist/core/debug.js.map +1 -1
- package/dist/core/init-mcp-server.d.ts.map +1 -1
- package/dist/core/init-mcp-server.js +9 -9
- package/dist/core/init-mcp-server.js.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +3 -3
- package/dist/core/logger.js.map +1 -1
- package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
- package/dist/core/mcp/create-mcp-server.js +1 -1
- package/dist/core/mcp/create-mcp-server.js.map +1 -1
- package/dist/core/mcp/prompts.d.ts.map +1 -1
- package/dist/core/mcp/prompts.js +1 -3
- package/dist/core/mcp/prompts.js.map +1 -1
- package/dist/core/mcp/resources.d.ts.map +1 -1
- package/dist/core/mcp/resources.js +8 -10
- package/dist/core/mcp/resources.js.map +1 -1
- package/dist/core/mcp/server-stdio.d.ts.map +1 -1
- package/dist/core/mcp/server-stdio.js.map +1 -1
- package/dist/core/utils/formatToolResult.d.ts.map +1 -1
- package/dist/core/utils/formatToolResult.js +1 -3
- package/dist/core/utils/formatToolResult.js.map +1 -1
- package/dist/core/utils/port-checker.d.ts.map +1 -1
- package/dist/core/utils/port-checker.js +1 -1
- package/dist/core/utils/port-checker.js.map +1 -1
- package/dist/core/utils/rate-limit.js +2 -2
- package/dist/core/utils/testing/McpSseClient.d.ts.map +1 -1
- package/dist/core/utils/testing/McpSseClient.js.map +1 -1
- package/dist/core/utils/testing/McpStdioClient.d.ts.map +1 -1
- package/dist/core/utils/testing/McpStdioClient.js.map +1 -1
- package/dist/core/utils/utils.d.ts.map +1 -1
- package/dist/core/utils/utils.js.map +1 -1
- package/dist/core/web/admin-router.d.ts.map +1 -1
- package/dist/core/web/admin-router.js +4 -4
- package/dist/core/web/admin-router.js.map +1 -1
- package/dist/core/web/cors.d.ts.map +1 -1
- package/dist/core/web/cors.js.map +1 -1
- package/dist/core/web/favicon-svg.d.ts.map +1 -1
- package/dist/core/web/favicon-svg.js.map +1 -1
- package/dist/core/web/home-api.d.ts.map +1 -1
- package/dist/core/web/home-api.js +4 -4
- package/dist/core/web/home-api.js.map +1 -1
- package/dist/core/web/openapi.d.ts.map +1 -1
- package/dist/core/web/openapi.js.map +1 -1
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +20 -22
- package/dist/core/web/server-http.js.map +1 -1
- package/dist/core/web/static/agent-tester/script.js +1503 -1513
- package/dist/core/web/static/home/script.js +646 -646
- package/dist/core/web/static/token-gen/script.js +561 -561
- package/dist/core/web/svg-icons.d.ts.map +1 -1
- package/dist/core/web/svg-icons.js +1 -1
- package/dist/core/web/svg-icons.js.map +1 -1
- package/package.json +2 -6
- package/scripts/copy-static.js +31 -31
- package/scripts/kill-port.js +107 -107
- package/scripts/npm/patch_node_modules.js +8 -8
- package/scripts/npm/run.js +31 -31
- package/scripts/remove-nul.js +53 -53
- package/scripts/update-doc.js +18 -18
- package/src/template/_types_/custom-config.ts +83 -83
- package/src/template/api/router.ts +86 -89
- package/src/template/custom-resources.ts +11 -11
- package/src/template/prompts/agent-brief.ts +8 -8
- package/src/template/prompts/agent-prompt.ts +10 -10
- package/src/template/prompts/custom-prompts.ts +12 -12
- package/src/template/start.ts +71 -72
- package/src/template/tools/handle-tool-call.ts +57 -56
- package/src/template/tools/tools.ts +89 -88
- package/src/tests/jest-simple-reporter.js +10 -10
- package/src/tests/mcp/sse/test-sse-npm-package.js +96 -96
- package/src/tests/mcp/test-cases.js +143 -143
- package/src/tests/mcp/test-http.js +76 -75
- package/src/tests/mcp/test-sse.js +80 -79
- package/src/tests/mcp/test-stdio.js +83 -81
- package/src/tests/utils.ts +157 -156
|
@@ -1,561 +1,561 @@
|
|
|
1
|
-
let keyValuePairCount = 0;
|
|
2
|
-
|
|
3
|
-
// ===========================
|
|
4
|
-
// Token Authentication Module
|
|
5
|
-
// ===========================
|
|
6
|
-
|
|
7
|
-
const AUTH_TOKEN_KEY = 'adminAuthToken';
|
|
8
|
-
let requiresBearerToken = false;
|
|
9
|
-
|
|
10
|
-
// Get stored auth token from sessionStorage
|
|
11
|
-
function getStoredToken () {
|
|
12
|
-
return sessionStorage.getItem(AUTH_TOKEN_KEY);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Store auth token in sessionStorage
|
|
16
|
-
function storeToken (token) {
|
|
17
|
-
sessionStorage.setItem(AUTH_TOKEN_KEY, token);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Clear stored auth token
|
|
21
|
-
function clearStoredToken () {
|
|
22
|
-
sessionStorage.removeItem(AUTH_TOKEN_KEY);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Show token authentication modal
|
|
26
|
-
function showTokenModal (errorMessage = null) {
|
|
27
|
-
const modal = document.getElementById('tokenModal');
|
|
28
|
-
const errorDiv = document.getElementById('tokenAuthError');
|
|
29
|
-
|
|
30
|
-
if (errorMessage) {
|
|
31
|
-
errorDiv.innerHTML = `<strong>Error:</strong> ${errorMessage}`;
|
|
32
|
-
errorDiv.style.display = 'block';
|
|
33
|
-
} else {
|
|
34
|
-
errorDiv.style.display = 'none';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
modal.style.display = 'flex';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Hide token authentication modal
|
|
41
|
-
function hideTokenModal () {
|
|
42
|
-
const modal = document.getElementById('tokenModal');
|
|
43
|
-
modal.style.display = 'none';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Authenticated fetch wrapper - adds Authorization header if token auth is required
|
|
47
|
-
async function authFetch (url, options = {}) {
|
|
48
|
-
const token = getStoredToken();
|
|
49
|
-
|
|
50
|
-
if (requiresBearerToken && token) {
|
|
51
|
-
options.headers = {
|
|
52
|
-
...options.headers,
|
|
53
|
-
'Authorization': `Bearer ${token}`,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const response = await fetch(url, options);
|
|
58
|
-
|
|
59
|
-
// Handle 401 Unauthorized - show token modal if available
|
|
60
|
-
if (response.status === 401 && requiresBearerToken) {
|
|
61
|
-
clearStoredToken();
|
|
62
|
-
const errorData = await response.json().catch(() => ({}));
|
|
63
|
-
const errorMessage = errorData.error || 'Authentication failed';
|
|
64
|
-
|
|
65
|
-
// Try to show modal, but if it's not available, throw with descriptive error
|
|
66
|
-
const modal = document.getElementById('tokenModal');
|
|
67
|
-
if (modal) {
|
|
68
|
-
showTokenModal(errorMessage + '. Please enter a valid token.');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Throw error with status code for form error handling
|
|
72
|
-
const error = new Error(`401 Unauthorized: ${errorMessage}`);
|
|
73
|
-
error.status = 401;
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return response;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Check auth config and initialize authentication if needed
|
|
81
|
-
async function initializeAuth () {
|
|
82
|
-
try {
|
|
83
|
-
// Get auth config from public endpoint (no auth required)
|
|
84
|
-
const response = await fetch('/admin/api/auth-config');
|
|
85
|
-
const config = await response.json();
|
|
86
|
-
|
|
87
|
-
if (config.success && config.requiresBearerToken) {
|
|
88
|
-
requiresBearerToken = true;
|
|
89
|
-
|
|
90
|
-
// Check if we have a stored token
|
|
91
|
-
const storedToken = getStoredToken();
|
|
92
|
-
if (!storedToken) {
|
|
93
|
-
showTokenModal();
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Verify token is still valid by making an authenticated request
|
|
98
|
-
try {
|
|
99
|
-
const verifyResponse = await authFetch('/admin/api/auth-status');
|
|
100
|
-
const verifyData = await verifyResponse.json();
|
|
101
|
-
|
|
102
|
-
if (!verifyData.success || !verifyData.isAuthenticated) {
|
|
103
|
-
clearStoredToken();
|
|
104
|
-
showTokenModal('Token is invalid or expired.');
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
// authFetch already handles 401 and shows modal
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return true;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error('Error checking auth config:', error);
|
|
116
|
-
return true; // Continue anyway if config check fails
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Handle token authentication form submission
|
|
121
|
-
function setupTokenAuthForm () {
|
|
122
|
-
const form = document.getElementById('tokenAuthForm');
|
|
123
|
-
if (!form) {return;}
|
|
124
|
-
|
|
125
|
-
form.addEventListener('submit', async (e) => {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
|
|
128
|
-
const tokenInput = document.getElementById('authTokenInput');
|
|
129
|
-
const token = tokenInput.value.trim();
|
|
130
|
-
|
|
131
|
-
if (!token) {
|
|
132
|
-
showTokenModal('Please enter a token.');
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Store token and try to authenticate
|
|
137
|
-
storeToken(token);
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const response = await authFetch('/admin/api/auth-status');
|
|
141
|
-
const data = await response.json();
|
|
142
|
-
|
|
143
|
-
if (data.success && data.isAuthenticated) {
|
|
144
|
-
hideTokenModal();
|
|
145
|
-
tokenInput.value = '';
|
|
146
|
-
// Reload auth status and initialize form
|
|
147
|
-
loadAuthStatus();
|
|
148
|
-
initializeForm();
|
|
149
|
-
} else {
|
|
150
|
-
clearStoredToken();
|
|
151
|
-
showTokenModal(data.error || 'Invalid token.');
|
|
152
|
-
}
|
|
153
|
-
} catch (error) {
|
|
154
|
-
// Error already handled in authFetch
|
|
155
|
-
if (error.message !== 'Unauthorized') {
|
|
156
|
-
clearStoredToken();
|
|
157
|
-
showTokenModal('Authentication failed: ' + error.message);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Set primary color CSS variable
|
|
164
|
-
function setPrimaryColor (color) {
|
|
165
|
-
if (color) {
|
|
166
|
-
document.documentElement.style.setProperty('--primary-color', color);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function switchTab (tabName) {
|
|
171
|
-
document.querySelectorAll('.tab-content').forEach(content => {
|
|
172
|
-
content.classList.remove('active');
|
|
173
|
-
});
|
|
174
|
-
document.querySelectorAll('.tab').forEach(tab => {
|
|
175
|
-
tab.classList.remove('active');
|
|
176
|
-
});
|
|
177
|
-
document.getElementById(tabName).classList.add('active');
|
|
178
|
-
|
|
179
|
-
// Activate the corresponding tab button
|
|
180
|
-
const tabs = document.querySelectorAll('.tab');
|
|
181
|
-
tabs.forEach(tab => {
|
|
182
|
-
const onclick = tab.getAttribute('onclick');
|
|
183
|
-
if (onclick && onclick.includes('switchTab(\'' + tabName + '\')')) {
|
|
184
|
-
tab.classList.add('active');
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function addKeyValuePair (key = '', value = '', readonly = false, placeholder = 'Value') {
|
|
190
|
-
if (keyValuePairCount >= 15) {
|
|
191
|
-
alert('Maximum of 15 key-value pairs');
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const container = document.getElementById('keyValuePairs');
|
|
195
|
-
const pairDiv = document.createElement('div');
|
|
196
|
-
pairDiv.className = 'key-value-pair';
|
|
197
|
-
|
|
198
|
-
const keyInput = readonly ?
|
|
199
|
-
'<input type="text" placeholder="Key" name="keys" value="' + key + '" readonly style="background-color: #f8f9fa;">' :
|
|
200
|
-
'<input type="text" placeholder="Key" name="keys" value="' + key + '">';
|
|
201
|
-
|
|
202
|
-
const valueInput = '<input type="text" placeholder="' + placeholder + '" name="values" value="' + value + '">';
|
|
203
|
-
|
|
204
|
-
pairDiv.innerHTML = keyInput + valueInput +
|
|
205
|
-
'<button type="button" class="remove-btn" onclick="removeKeyValuePair(this)">×</button>';
|
|
206
|
-
container.appendChild(pairDiv);
|
|
207
|
-
keyValuePairCount++;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
211
|
-
function removeKeyValuePair (button) {
|
|
212
|
-
button.parentElement.remove();
|
|
213
|
-
keyValuePairCount--;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function addCopyButtonToTokenOutput (tokenOutput, token) {
|
|
217
|
-
if (!tokenOutput || tokenOutput.hasAttribute('data-copy-added')) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
tokenOutput.setAttribute('data-copy-added', 'true');
|
|
222
|
-
|
|
223
|
-
const copyButton = document.createElement('button');
|
|
224
|
-
copyButton.className = 'copy-button';
|
|
225
|
-
copyButton.innerHTML = '📋';
|
|
226
|
-
copyButton.title = 'Copy to clipboard';
|
|
227
|
-
copyButton.setAttribute('aria-label', 'Copy to clipboard');
|
|
228
|
-
|
|
229
|
-
const validateButton = document.createElement('button');
|
|
230
|
-
validateButton.className = 'validate-token-button';
|
|
231
|
-
validateButton.innerHTML = '✓';
|
|
232
|
-
validateButton.title = 'Validate token';
|
|
233
|
-
validateButton.setAttribute('aria-label', 'Validate token');
|
|
234
|
-
|
|
235
|
-
const notification = document.createElement('div');
|
|
236
|
-
notification.className = 'copy-notification';
|
|
237
|
-
notification.textContent = 'Copied';
|
|
238
|
-
|
|
239
|
-
tokenOutput.appendChild(copyButton);
|
|
240
|
-
tokenOutput.appendChild(validateButton);
|
|
241
|
-
tokenOutput.appendChild(notification);
|
|
242
|
-
|
|
243
|
-
copyButton.addEventListener('click', async function () {
|
|
244
|
-
try {
|
|
245
|
-
await navigator.clipboard.writeText(token);
|
|
246
|
-
|
|
247
|
-
// Show notification
|
|
248
|
-
notification.classList.add('show');
|
|
249
|
-
|
|
250
|
-
// Hide notification after 1 second
|
|
251
|
-
setTimeout(() => {
|
|
252
|
-
notification.classList.remove('show');
|
|
253
|
-
}, 1000);
|
|
254
|
-
|
|
255
|
-
} catch {
|
|
256
|
-
// Fallback for browsers that don't support clipboard API
|
|
257
|
-
const textArea = document.createElement('textarea');
|
|
258
|
-
textArea.value = token;
|
|
259
|
-
textArea.style.position = 'fixed';
|
|
260
|
-
textArea.style.opacity = '0';
|
|
261
|
-
document.body.appendChild(textArea);
|
|
262
|
-
textArea.focus();
|
|
263
|
-
textArea.select();
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
document.execCommand('copy');
|
|
267
|
-
|
|
268
|
-
// Show notification
|
|
269
|
-
notification.classList.add('show');
|
|
270
|
-
|
|
271
|
-
// Hide notification after 1 second
|
|
272
|
-
setTimeout(() => {
|
|
273
|
-
notification.classList.remove('show');
|
|
274
|
-
}, 1000);
|
|
275
|
-
} catch (fallbackErr) {
|
|
276
|
-
console.error('Failed to copy text:', fallbackErr);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
document.body.removeChild(textArea);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
validateButton.addEventListener('click', function () {
|
|
284
|
-
// Switch to validation tab
|
|
285
|
-
switchTab('validate');
|
|
286
|
-
|
|
287
|
-
// Set the token in the validation textarea
|
|
288
|
-
const tokenTextarea = document.getElementById('tokenInput');
|
|
289
|
-
if (tokenTextarea) {
|
|
290
|
-
tokenTextarea.value = token;
|
|
291
|
-
|
|
292
|
-
// Trigger validation form submit
|
|
293
|
-
const validateForm = document.getElementById('validateForm');
|
|
294
|
-
if (validateForm) {
|
|
295
|
-
validateForm.dispatchEvent(new Event('submit'));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function formatTime (ms) {
|
|
302
|
-
const seconds = Math.floor(ms / 1000);
|
|
303
|
-
const minutes = Math.floor(seconds / 60);
|
|
304
|
-
const hours = Math.floor(minutes / 60);
|
|
305
|
-
const days = Math.floor(hours / 24);
|
|
306
|
-
|
|
307
|
-
if (days > 0) {return days + ' d. ' + (hours % 24) + ' h.';}
|
|
308
|
-
if (hours > 0) {return hours + ' h. ' + (minutes % 60) + ' min.';}
|
|
309
|
-
if (minutes > 0) {return minutes + ' min.';}
|
|
310
|
-
return seconds + ' s.';
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Render authentication status in header
|
|
314
|
-
function renderAuthStatus (data) {
|
|
315
|
-
const container = document.getElementById('authStatusContainer');
|
|
316
|
-
if (!container) {return;}
|
|
317
|
-
|
|
318
|
-
const { authType, isAuthenticated, user, canLogout } = data;
|
|
319
|
-
|
|
320
|
-
// Don't show anything if no auth is configured or not authenticated
|
|
321
|
-
if (!authType || !isAuthenticated || !user) {
|
|
322
|
-
container.style.display = 'none';
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
let html = '<div class="header-auth-info">';
|
|
327
|
-
html += '<img src="/svg/token-gen/user.svg" alt="" class="user-icon">';
|
|
328
|
-
html += '<span class="username">' + user + '</span>';
|
|
329
|
-
html += '</div>';
|
|
330
|
-
|
|
331
|
-
// Show logout button only for basic and ntlm auth types
|
|
332
|
-
if (canLogout) {
|
|
333
|
-
html += '<button class="header-logout-btn" onclick="logout()" title="Log out">';
|
|
334
|
-
html += '<img src="/svg/token-gen/logout.svg" alt="Log out">';
|
|
335
|
-
html += '</button>';
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
container.innerHTML = html;
|
|
339
|
-
container.style.display = 'flex';
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Load authentication status from API
|
|
343
|
-
async function loadAuthStatus () {
|
|
344
|
-
try {
|
|
345
|
-
const response = await authFetch('/admin/api/auth-status');
|
|
346
|
-
const data = await response.json();
|
|
347
|
-
if (data.success) {
|
|
348
|
-
renderAuthStatus(data);
|
|
349
|
-
}
|
|
350
|
-
} catch (error) {
|
|
351
|
-
console.error('Error loading auth status:', error);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Processing the Generation Form
|
|
356
|
-
document.getElementById('generateForm').addEventListener('submit', async (e) => {
|
|
357
|
-
e.preventDefault();
|
|
358
|
-
|
|
359
|
-
const formData = new FormData(e.target);
|
|
360
|
-
const keys = formData.getAll('keys').filter(k => k.trim());
|
|
361
|
-
const values = formData.getAll('values').filter(v => v.trim());
|
|
362
|
-
|
|
363
|
-
const payload = {};
|
|
364
|
-
for (let i = 0; i < keys.length; i++) {
|
|
365
|
-
if (keys[i] && values[i]) {
|
|
366
|
-
payload[keys[i]] = values[i];
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const ipValue = document.getElementById('tokenIp').value.trim();
|
|
371
|
-
if (ipValue) {
|
|
372
|
-
payload.ip = ipValue;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const requestData = {
|
|
376
|
-
user: formData.get('user'),
|
|
377
|
-
timeValue: parseInt(formData.get('timeValue')),
|
|
378
|
-
timeUnit: formData.get('timeUnit'),
|
|
379
|
-
payload: payload,
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
const response = await authFetch('/admin/api/generate-token', {
|
|
384
|
-
method: 'POST',
|
|
385
|
-
headers: { 'Content-Type': 'application/json' },
|
|
386
|
-
body: JSON.stringify(requestData),
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
const result = await response.json();
|
|
390
|
-
const resultDiv = document.getElementById('generateResult');
|
|
391
|
-
|
|
392
|
-
if (result.success) {
|
|
393
|
-
resultDiv.innerHTML =
|
|
394
|
-
`<div class="result success">
|
|
395
|
-
<strong>The token has been successfully created!</strong><br>
|
|
396
|
-
<div class="token-output">${result.token}</div>
|
|
397
|
-
</div>`;
|
|
398
|
-
|
|
399
|
-
// Add floating copy button to the token output
|
|
400
|
-
const tokenOutput = resultDiv.querySelector('.token-output');
|
|
401
|
-
if (tokenOutput) {
|
|
402
|
-
addCopyButtonToTokenOutput(tokenOutput, result.token);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Automatically populate the validation field with the generated token
|
|
406
|
-
const tokenTextarea = document.getElementById('tokenInput');
|
|
407
|
-
if (tokenTextarea) {
|
|
408
|
-
tokenTextarea.value = result.token;
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
resultDiv.innerHTML =
|
|
412
|
-
`<div class="result error">
|
|
413
|
-
<strong>Error:</strong> ${result.error}
|
|
414
|
-
</div>`;
|
|
415
|
-
}
|
|
416
|
-
} catch (error) {
|
|
417
|
-
document.getElementById('generateResult').innerHTML =
|
|
418
|
-
`<div class="result error">
|
|
419
|
-
<strong>Error:</strong> ${error.message}
|
|
420
|
-
</div>`;
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// Processing the Verification Form
|
|
425
|
-
document.getElementById('validateForm').addEventListener('submit', async (e) => {
|
|
426
|
-
e.preventDefault();
|
|
427
|
-
|
|
428
|
-
const formData = new FormData(e.target);
|
|
429
|
-
const token = formData.get('token').trim();
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
const response = await authFetch('/admin/api/validate-token', {
|
|
433
|
-
method: 'POST',
|
|
434
|
-
headers: { 'Content-Type': 'application/json' },
|
|
435
|
-
body: JSON.stringify({ token }),
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
const result = await response.json();
|
|
439
|
-
const resultDiv = document.getElementById('validateResult');
|
|
440
|
-
|
|
441
|
-
if (result.success) {
|
|
442
|
-
const remainingTime = result.payload.expire - Date.now();
|
|
443
|
-
const payloadKeys = Object.keys(result.payload).filter((k) => !/^(user|expire|iat|service|ip)$/.test(k));
|
|
444
|
-
|
|
445
|
-
let payloadHtml = '';
|
|
446
|
-
if (payloadKeys.length > 0) {
|
|
447
|
-
payloadHtml = '<h4>Additional data:</h4>';
|
|
448
|
-
payloadKeys.forEach(key => {
|
|
449
|
-
payloadHtml += '<p><strong>' + key + ':</strong> ' + result.payload[key] + '</p>';
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Format issued at time
|
|
454
|
-
const issuedAtTime = result.payload.iat ? new Date(result.payload.iat).toLocaleString('ru-RU') : 'N/A';
|
|
455
|
-
|
|
456
|
-
resultDiv.innerHTML =
|
|
457
|
-
`<div class="result success">
|
|
458
|
-
<strong>The token is valid!</strong>
|
|
459
|
-
<div class="token-info">
|
|
460
|
-
<h4>Token Information:</h4>
|
|
461
|
-
<p><strong>User:</strong> ${result.payload.user}</p>
|
|
462
|
-
${result.payload.service ? `<p><strong>Service:</strong> ${result.payload.service}</p>` : ''}
|
|
463
|
-
${result.payload.ip ? `<p><strong>Allowed IPs:</strong> ${result.payload.ip}</p>` : ''}
|
|
464
|
-
<p><strong>Issued at:</strong> ${issuedAtTime}</p>
|
|
465
|
-
<p><strong>Time remaining:</strong> ${formatTime(remainingTime)}</p>
|
|
466
|
-
<p><strong>Expires:</strong> ${new Date(result.payload.expire).toLocaleString('ru-RU')}</p>
|
|
467
|
-
${payloadHtml}
|
|
468
|
-
</div>
|
|
469
|
-
</div>`;
|
|
470
|
-
} else {
|
|
471
|
-
resultDiv.innerHTML =
|
|
472
|
-
`<div class="result error">
|
|
473
|
-
<strong>Token invalid!</strong><br>
|
|
474
|
-
Reason: ${result.error}
|
|
475
|
-
</div>`;
|
|
476
|
-
}
|
|
477
|
-
} catch (error) {
|
|
478
|
-
document.getElementById('validateResult').innerHTML =
|
|
479
|
-
`<div class="result error">
|
|
480
|
-
<strong>Error:</strong> ${error.message}
|
|
481
|
-
</div>`;
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// Function to initialize the form
|
|
486
|
-
async function initializeForm () {
|
|
487
|
-
try {
|
|
488
|
-
// Getting information about the service
|
|
489
|
-
const response = await authFetch('/admin/api/service-info');
|
|
490
|
-
const data = await response.json();
|
|
491
|
-
const serviceName = data
|
|
492
|
-
|
|
493
|
-
// Set theme color
|
|
494
|
-
setPrimaryColor(data.primaryColor);
|
|
495
|
-
|
|
496
|
-
// Clear existing key-value pairs before re-initializing
|
|
497
|
-
const container = document.getElementById('keyValuePairs');
|
|
498
|
-
container.innerHTML = '';
|
|
499
|
-
keyValuePairCount = 0;
|
|
500
|
-
|
|
501
|
-
// Adding a pre-filled pair serviceName
|
|
502
|
-
addKeyValuePair('service', serviceName, true);
|
|
503
|
-
addKeyValuePair('issue', '', true, 'URL of request for the issuance of a token in JIRA');
|
|
504
|
-
|
|
505
|
-
} catch (error) {
|
|
506
|
-
console.error('Error loading service info:', error);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
// Add one empty pair for the user
|
|
510
|
-
addKeyValuePair();
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Logout function
|
|
514
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
515
|
-
async function logout () {
|
|
516
|
-
try {
|
|
517
|
-
// For token-based auth, just clear the stored token
|
|
518
|
-
if (requiresBearerToken) {
|
|
519
|
-
clearStoredToken();
|
|
520
|
-
showTokenModal();
|
|
521
|
-
// Clear auth status display
|
|
522
|
-
const container = document.getElementById('authStatusContainer');
|
|
523
|
-
if (container) {
|
|
524
|
-
container.style.display = 'none';
|
|
525
|
-
}
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// For other auth types (NTLM, Basic), make logout request
|
|
530
|
-
const response = await fetch('/admin/logout', {
|
|
531
|
-
method: 'GET',
|
|
532
|
-
credentials: 'include',
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
if (response.status === 401) {
|
|
536
|
-
// Authentication cleared, reload page to trigger browser auth prompt
|
|
537
|
-
window.location.reload();
|
|
538
|
-
} else {
|
|
539
|
-
console.error('Logout failed');
|
|
540
|
-
alert('Logout failed. Please clear your browser cache and reload the page.');
|
|
541
|
-
}
|
|
542
|
-
} catch (error) {
|
|
543
|
-
console.error('Error during logout:', error);
|
|
544
|
-
alert('Error during logout. Please clear your browser cache and reload the page.');
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Initialization on page load
|
|
549
|
-
document.addEventListener('DOMContentLoaded', async () => {
|
|
550
|
-
// Setup token auth form handler
|
|
551
|
-
setupTokenAuthForm();
|
|
552
|
-
|
|
553
|
-
// Initialize authentication (check if token is needed and valid)
|
|
554
|
-
const authOk = await initializeAuth();
|
|
555
|
-
|
|
556
|
-
if (authOk) {
|
|
557
|
-
// Load auth status and form only if authenticated
|
|
558
|
-
loadAuthStatus();
|
|
559
|
-
initializeForm();
|
|
560
|
-
}
|
|
561
|
-
});
|
|
1
|
+
let keyValuePairCount = 0;
|
|
2
|
+
|
|
3
|
+
// ===========================
|
|
4
|
+
// Token Authentication Module
|
|
5
|
+
// ===========================
|
|
6
|
+
|
|
7
|
+
const AUTH_TOKEN_KEY = 'adminAuthToken';
|
|
8
|
+
let requiresBearerToken = false;
|
|
9
|
+
|
|
10
|
+
// Get stored auth token from sessionStorage
|
|
11
|
+
function getStoredToken () {
|
|
12
|
+
return sessionStorage.getItem(AUTH_TOKEN_KEY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Store auth token in sessionStorage
|
|
16
|
+
function storeToken (token) {
|
|
17
|
+
sessionStorage.setItem(AUTH_TOKEN_KEY, token);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Clear stored auth token
|
|
21
|
+
function clearStoredToken () {
|
|
22
|
+
sessionStorage.removeItem(AUTH_TOKEN_KEY);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Show token authentication modal
|
|
26
|
+
function showTokenModal (errorMessage = null) {
|
|
27
|
+
const modal = document.getElementById('tokenModal');
|
|
28
|
+
const errorDiv = document.getElementById('tokenAuthError');
|
|
29
|
+
|
|
30
|
+
if (errorMessage) {
|
|
31
|
+
errorDiv.innerHTML = `<strong>Error:</strong> ${errorMessage}`;
|
|
32
|
+
errorDiv.style.display = 'block';
|
|
33
|
+
} else {
|
|
34
|
+
errorDiv.style.display = 'none';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
modal.style.display = 'flex';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Hide token authentication modal
|
|
41
|
+
function hideTokenModal () {
|
|
42
|
+
const modal = document.getElementById('tokenModal');
|
|
43
|
+
modal.style.display = 'none';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Authenticated fetch wrapper - adds Authorization header if token auth is required
|
|
47
|
+
async function authFetch (url, options = {}) {
|
|
48
|
+
const token = getStoredToken();
|
|
49
|
+
|
|
50
|
+
if (requiresBearerToken && token) {
|
|
51
|
+
options.headers = {
|
|
52
|
+
...options.headers,
|
|
53
|
+
'Authorization': `Bearer ${token}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = await fetch(url, options);
|
|
58
|
+
|
|
59
|
+
// Handle 401 Unauthorized - show token modal if available
|
|
60
|
+
if (response.status === 401 && requiresBearerToken) {
|
|
61
|
+
clearStoredToken();
|
|
62
|
+
const errorData = await response.json().catch(() => ({}));
|
|
63
|
+
const errorMessage = errorData.error || 'Authentication failed';
|
|
64
|
+
|
|
65
|
+
// Try to show modal, but if it's not available, throw with descriptive error
|
|
66
|
+
const modal = document.getElementById('tokenModal');
|
|
67
|
+
if (modal) {
|
|
68
|
+
showTokenModal(errorMessage + '. Please enter a valid token.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Throw error with status code for form error handling
|
|
72
|
+
const error = new Error(`401 Unauthorized: ${errorMessage}`);
|
|
73
|
+
error.status = 401;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check auth config and initialize authentication if needed
|
|
81
|
+
async function initializeAuth () {
|
|
82
|
+
try {
|
|
83
|
+
// Get auth config from public endpoint (no auth required)
|
|
84
|
+
const response = await fetch('/admin/api/auth-config');
|
|
85
|
+
const config = await response.json();
|
|
86
|
+
|
|
87
|
+
if (config.success && config.requiresBearerToken) {
|
|
88
|
+
requiresBearerToken = true;
|
|
89
|
+
|
|
90
|
+
// Check if we have a stored token
|
|
91
|
+
const storedToken = getStoredToken();
|
|
92
|
+
if (!storedToken) {
|
|
93
|
+
showTokenModal();
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify token is still valid by making an authenticated request
|
|
98
|
+
try {
|
|
99
|
+
const verifyResponse = await authFetch('/admin/api/auth-status');
|
|
100
|
+
const verifyData = await verifyResponse.json();
|
|
101
|
+
|
|
102
|
+
if (!verifyData.success || !verifyData.isAuthenticated) {
|
|
103
|
+
clearStoredToken();
|
|
104
|
+
showTokenModal('Token is invalid or expired.');
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// authFetch already handles 401 and shows modal
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('Error checking auth config:', error);
|
|
116
|
+
return true; // Continue anyway if config check fails
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle token authentication form submission
|
|
121
|
+
function setupTokenAuthForm () {
|
|
122
|
+
const form = document.getElementById('tokenAuthForm');
|
|
123
|
+
if (!form) {return;}
|
|
124
|
+
|
|
125
|
+
form.addEventListener('submit', async (e) => {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
|
|
128
|
+
const tokenInput = document.getElementById('authTokenInput');
|
|
129
|
+
const token = tokenInput.value.trim();
|
|
130
|
+
|
|
131
|
+
if (!token) {
|
|
132
|
+
showTokenModal('Please enter a token.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Store token and try to authenticate
|
|
137
|
+
storeToken(token);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const response = await authFetch('/admin/api/auth-status');
|
|
141
|
+
const data = await response.json();
|
|
142
|
+
|
|
143
|
+
if (data.success && data.isAuthenticated) {
|
|
144
|
+
hideTokenModal();
|
|
145
|
+
tokenInput.value = '';
|
|
146
|
+
// Reload auth status and initialize form
|
|
147
|
+
loadAuthStatus();
|
|
148
|
+
initializeForm();
|
|
149
|
+
} else {
|
|
150
|
+
clearStoredToken();
|
|
151
|
+
showTokenModal(data.error || 'Invalid token.');
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// Error already handled in authFetch
|
|
155
|
+
if (error.message !== 'Unauthorized') {
|
|
156
|
+
clearStoredToken();
|
|
157
|
+
showTokenModal('Authentication failed: ' + error.message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Set primary color CSS variable
|
|
164
|
+
function setPrimaryColor (color) {
|
|
165
|
+
if (color) {
|
|
166
|
+
document.documentElement.style.setProperty('--primary-color', color);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function switchTab (tabName) {
|
|
171
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
172
|
+
content.classList.remove('active');
|
|
173
|
+
});
|
|
174
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
175
|
+
tab.classList.remove('active');
|
|
176
|
+
});
|
|
177
|
+
document.getElementById(tabName).classList.add('active');
|
|
178
|
+
|
|
179
|
+
// Activate the corresponding tab button
|
|
180
|
+
const tabs = document.querySelectorAll('.tab');
|
|
181
|
+
tabs.forEach(tab => {
|
|
182
|
+
const onclick = tab.getAttribute('onclick');
|
|
183
|
+
if (onclick && onclick.includes('switchTab(\'' + tabName + '\')')) {
|
|
184
|
+
tab.classList.add('active');
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function addKeyValuePair (key = '', value = '', readonly = false, placeholder = 'Value') {
|
|
190
|
+
if (keyValuePairCount >= 15) {
|
|
191
|
+
alert('Maximum of 15 key-value pairs');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const container = document.getElementById('keyValuePairs');
|
|
195
|
+
const pairDiv = document.createElement('div');
|
|
196
|
+
pairDiv.className = 'key-value-pair';
|
|
197
|
+
|
|
198
|
+
const keyInput = readonly ?
|
|
199
|
+
'<input type="text" placeholder="Key" name="keys" value="' + key + '" readonly style="background-color: #f8f9fa;">' :
|
|
200
|
+
'<input type="text" placeholder="Key" name="keys" value="' + key + '">';
|
|
201
|
+
|
|
202
|
+
const valueInput = '<input type="text" placeholder="' + placeholder + '" name="values" value="' + value + '">';
|
|
203
|
+
|
|
204
|
+
pairDiv.innerHTML = keyInput + valueInput +
|
|
205
|
+
'<button type="button" class="remove-btn" onclick="removeKeyValuePair(this)">×</button>';
|
|
206
|
+
container.appendChild(pairDiv);
|
|
207
|
+
keyValuePairCount++;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
211
|
+
function removeKeyValuePair (button) {
|
|
212
|
+
button.parentElement.remove();
|
|
213
|
+
keyValuePairCount--;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function addCopyButtonToTokenOutput (tokenOutput, token) {
|
|
217
|
+
if (!tokenOutput || tokenOutput.hasAttribute('data-copy-added')) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
tokenOutput.setAttribute('data-copy-added', 'true');
|
|
222
|
+
|
|
223
|
+
const copyButton = document.createElement('button');
|
|
224
|
+
copyButton.className = 'copy-button';
|
|
225
|
+
copyButton.innerHTML = '📋';
|
|
226
|
+
copyButton.title = 'Copy to clipboard';
|
|
227
|
+
copyButton.setAttribute('aria-label', 'Copy to clipboard');
|
|
228
|
+
|
|
229
|
+
const validateButton = document.createElement('button');
|
|
230
|
+
validateButton.className = 'validate-token-button';
|
|
231
|
+
validateButton.innerHTML = '✓';
|
|
232
|
+
validateButton.title = 'Validate token';
|
|
233
|
+
validateButton.setAttribute('aria-label', 'Validate token');
|
|
234
|
+
|
|
235
|
+
const notification = document.createElement('div');
|
|
236
|
+
notification.className = 'copy-notification';
|
|
237
|
+
notification.textContent = 'Copied';
|
|
238
|
+
|
|
239
|
+
tokenOutput.appendChild(copyButton);
|
|
240
|
+
tokenOutput.appendChild(validateButton);
|
|
241
|
+
tokenOutput.appendChild(notification);
|
|
242
|
+
|
|
243
|
+
copyButton.addEventListener('click', async function () {
|
|
244
|
+
try {
|
|
245
|
+
await navigator.clipboard.writeText(token);
|
|
246
|
+
|
|
247
|
+
// Show notification
|
|
248
|
+
notification.classList.add('show');
|
|
249
|
+
|
|
250
|
+
// Hide notification after 1 second
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
notification.classList.remove('show');
|
|
253
|
+
}, 1000);
|
|
254
|
+
|
|
255
|
+
} catch {
|
|
256
|
+
// Fallback for browsers that don't support clipboard API
|
|
257
|
+
const textArea = document.createElement('textarea');
|
|
258
|
+
textArea.value = token;
|
|
259
|
+
textArea.style.position = 'fixed';
|
|
260
|
+
textArea.style.opacity = '0';
|
|
261
|
+
document.body.appendChild(textArea);
|
|
262
|
+
textArea.focus();
|
|
263
|
+
textArea.select();
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
document.execCommand('copy');
|
|
267
|
+
|
|
268
|
+
// Show notification
|
|
269
|
+
notification.classList.add('show');
|
|
270
|
+
|
|
271
|
+
// Hide notification after 1 second
|
|
272
|
+
setTimeout(() => {
|
|
273
|
+
notification.classList.remove('show');
|
|
274
|
+
}, 1000);
|
|
275
|
+
} catch (fallbackErr) {
|
|
276
|
+
console.error('Failed to copy text:', fallbackErr);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
document.body.removeChild(textArea);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
validateButton.addEventListener('click', function () {
|
|
284
|
+
// Switch to validation tab
|
|
285
|
+
switchTab('validate');
|
|
286
|
+
|
|
287
|
+
// Set the token in the validation textarea
|
|
288
|
+
const tokenTextarea = document.getElementById('tokenInput');
|
|
289
|
+
if (tokenTextarea) {
|
|
290
|
+
tokenTextarea.value = token;
|
|
291
|
+
|
|
292
|
+
// Trigger validation form submit
|
|
293
|
+
const validateForm = document.getElementById('validateForm');
|
|
294
|
+
if (validateForm) {
|
|
295
|
+
validateForm.dispatchEvent(new Event('submit'));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function formatTime (ms) {
|
|
302
|
+
const seconds = Math.floor(ms / 1000);
|
|
303
|
+
const minutes = Math.floor(seconds / 60);
|
|
304
|
+
const hours = Math.floor(minutes / 60);
|
|
305
|
+
const days = Math.floor(hours / 24);
|
|
306
|
+
|
|
307
|
+
if (days > 0) {return days + ' d. ' + (hours % 24) + ' h.';}
|
|
308
|
+
if (hours > 0) {return hours + ' h. ' + (minutes % 60) + ' min.';}
|
|
309
|
+
if (minutes > 0) {return minutes + ' min.';}
|
|
310
|
+
return seconds + ' s.';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Render authentication status in header
|
|
314
|
+
function renderAuthStatus (data) {
|
|
315
|
+
const container = document.getElementById('authStatusContainer');
|
|
316
|
+
if (!container) {return;}
|
|
317
|
+
|
|
318
|
+
const { authType, isAuthenticated, user, canLogout } = data;
|
|
319
|
+
|
|
320
|
+
// Don't show anything if no auth is configured or not authenticated
|
|
321
|
+
if (!authType || !isAuthenticated || !user) {
|
|
322
|
+
container.style.display = 'none';
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let html = '<div class="header-auth-info">';
|
|
327
|
+
html += '<img src="/svg/token-gen/user.svg" alt="" class="user-icon">';
|
|
328
|
+
html += '<span class="username">' + user + '</span>';
|
|
329
|
+
html += '</div>';
|
|
330
|
+
|
|
331
|
+
// Show logout button only for basic and ntlm auth types
|
|
332
|
+
if (canLogout) {
|
|
333
|
+
html += '<button class="header-logout-btn" onclick="logout()" title="Log out">';
|
|
334
|
+
html += '<img src="/svg/token-gen/logout.svg" alt="Log out">';
|
|
335
|
+
html += '</button>';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
container.innerHTML = html;
|
|
339
|
+
container.style.display = 'flex';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Load authentication status from API
|
|
343
|
+
async function loadAuthStatus () {
|
|
344
|
+
try {
|
|
345
|
+
const response = await authFetch('/admin/api/auth-status');
|
|
346
|
+
const data = await response.json();
|
|
347
|
+
if (data.success) {
|
|
348
|
+
renderAuthStatus(data);
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('Error loading auth status:', error);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Processing the Generation Form
|
|
356
|
+
document.getElementById('generateForm').addEventListener('submit', async (e) => {
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
|
|
359
|
+
const formData = new FormData(e.target);
|
|
360
|
+
const keys = formData.getAll('keys').filter(k => k.trim());
|
|
361
|
+
const values = formData.getAll('values').filter(v => v.trim());
|
|
362
|
+
|
|
363
|
+
const payload = {};
|
|
364
|
+
for (let i = 0; i < keys.length; i++) {
|
|
365
|
+
if (keys[i] && values[i]) {
|
|
366
|
+
payload[keys[i]] = values[i];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const ipValue = document.getElementById('tokenIp').value.trim();
|
|
371
|
+
if (ipValue) {
|
|
372
|
+
payload.ip = ipValue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const requestData = {
|
|
376
|
+
user: formData.get('user'),
|
|
377
|
+
timeValue: parseInt(formData.get('timeValue')),
|
|
378
|
+
timeUnit: formData.get('timeUnit'),
|
|
379
|
+
payload: payload,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const response = await authFetch('/admin/api/generate-token', {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
386
|
+
body: JSON.stringify(requestData),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const result = await response.json();
|
|
390
|
+
const resultDiv = document.getElementById('generateResult');
|
|
391
|
+
|
|
392
|
+
if (result.success) {
|
|
393
|
+
resultDiv.innerHTML =
|
|
394
|
+
`<div class="result success">
|
|
395
|
+
<strong>The token has been successfully created!</strong><br>
|
|
396
|
+
<div class="token-output">${result.token}</div>
|
|
397
|
+
</div>`;
|
|
398
|
+
|
|
399
|
+
// Add floating copy button to the token output
|
|
400
|
+
const tokenOutput = resultDiv.querySelector('.token-output');
|
|
401
|
+
if (tokenOutput) {
|
|
402
|
+
addCopyButtonToTokenOutput(tokenOutput, result.token);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Automatically populate the validation field with the generated token
|
|
406
|
+
const tokenTextarea = document.getElementById('tokenInput');
|
|
407
|
+
if (tokenTextarea) {
|
|
408
|
+
tokenTextarea.value = result.token;
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
resultDiv.innerHTML =
|
|
412
|
+
`<div class="result error">
|
|
413
|
+
<strong>Error:</strong> ${result.error}
|
|
414
|
+
</div>`;
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
document.getElementById('generateResult').innerHTML =
|
|
418
|
+
`<div class="result error">
|
|
419
|
+
<strong>Error:</strong> ${error.message}
|
|
420
|
+
</div>`;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Processing the Verification Form
|
|
425
|
+
document.getElementById('validateForm').addEventListener('submit', async (e) => {
|
|
426
|
+
e.preventDefault();
|
|
427
|
+
|
|
428
|
+
const formData = new FormData(e.target);
|
|
429
|
+
const token = formData.get('token').trim();
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const response = await authFetch('/admin/api/validate-token', {
|
|
433
|
+
method: 'POST',
|
|
434
|
+
headers: { 'Content-Type': 'application/json' },
|
|
435
|
+
body: JSON.stringify({ token }),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const result = await response.json();
|
|
439
|
+
const resultDiv = document.getElementById('validateResult');
|
|
440
|
+
|
|
441
|
+
if (result.success) {
|
|
442
|
+
const remainingTime = result.payload.expire - Date.now();
|
|
443
|
+
const payloadKeys = Object.keys(result.payload).filter((k) => !/^(user|expire|iat|service|ip)$/.test(k));
|
|
444
|
+
|
|
445
|
+
let payloadHtml = '';
|
|
446
|
+
if (payloadKeys.length > 0) {
|
|
447
|
+
payloadHtml = '<h4>Additional data:</h4>';
|
|
448
|
+
payloadKeys.forEach(key => {
|
|
449
|
+
payloadHtml += '<p><strong>' + key + ':</strong> ' + result.payload[key] + '</p>';
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Format issued at time
|
|
454
|
+
const issuedAtTime = result.payload.iat ? new Date(result.payload.iat).toLocaleString('ru-RU') : 'N/A';
|
|
455
|
+
|
|
456
|
+
resultDiv.innerHTML =
|
|
457
|
+
`<div class="result success">
|
|
458
|
+
<strong>The token is valid!</strong>
|
|
459
|
+
<div class="token-info">
|
|
460
|
+
<h4>Token Information:</h4>
|
|
461
|
+
<p><strong>User:</strong> ${result.payload.user}</p>
|
|
462
|
+
${result.payload.service ? `<p><strong>Service:</strong> ${result.payload.service}</p>` : ''}
|
|
463
|
+
${result.payload.ip ? `<p><strong>Allowed IPs:</strong> ${result.payload.ip}</p>` : ''}
|
|
464
|
+
<p><strong>Issued at:</strong> ${issuedAtTime}</p>
|
|
465
|
+
<p><strong>Time remaining:</strong> ${formatTime(remainingTime)}</p>
|
|
466
|
+
<p><strong>Expires:</strong> ${new Date(result.payload.expire).toLocaleString('ru-RU')}</p>
|
|
467
|
+
${payloadHtml}
|
|
468
|
+
</div>
|
|
469
|
+
</div>`;
|
|
470
|
+
} else {
|
|
471
|
+
resultDiv.innerHTML =
|
|
472
|
+
`<div class="result error">
|
|
473
|
+
<strong>Token invalid!</strong><br>
|
|
474
|
+
Reason: ${result.error}
|
|
475
|
+
</div>`;
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
document.getElementById('validateResult').innerHTML =
|
|
479
|
+
`<div class="result error">
|
|
480
|
+
<strong>Error:</strong> ${error.message}
|
|
481
|
+
</div>`;
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Function to initialize the form
|
|
486
|
+
async function initializeForm () {
|
|
487
|
+
try {
|
|
488
|
+
// Getting information about the service
|
|
489
|
+
const response = await authFetch('/admin/api/service-info');
|
|
490
|
+
const data = await response.json();
|
|
491
|
+
const { serviceName } = data;
|
|
492
|
+
|
|
493
|
+
// Set theme color
|
|
494
|
+
setPrimaryColor(data.primaryColor);
|
|
495
|
+
|
|
496
|
+
// Clear existing key-value pairs before re-initializing
|
|
497
|
+
const container = document.getElementById('keyValuePairs');
|
|
498
|
+
container.innerHTML = '';
|
|
499
|
+
keyValuePairCount = 0;
|
|
500
|
+
|
|
501
|
+
// Adding a pre-filled pair serviceName
|
|
502
|
+
addKeyValuePair('service', serviceName, true);
|
|
503
|
+
addKeyValuePair('issue', '', true, 'URL of request for the issuance of a token in JIRA');
|
|
504
|
+
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.error('Error loading service info:', error);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// Add one empty pair for the user
|
|
510
|
+
addKeyValuePair();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Logout function
|
|
514
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
515
|
+
async function logout () {
|
|
516
|
+
try {
|
|
517
|
+
// For token-based auth, just clear the stored token
|
|
518
|
+
if (requiresBearerToken) {
|
|
519
|
+
clearStoredToken();
|
|
520
|
+
showTokenModal();
|
|
521
|
+
// Clear auth status display
|
|
522
|
+
const container = document.getElementById('authStatusContainer');
|
|
523
|
+
if (container) {
|
|
524
|
+
container.style.display = 'none';
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// For other auth types (NTLM, Basic), make logout request
|
|
530
|
+
const response = await fetch('/admin/logout', {
|
|
531
|
+
method: 'GET',
|
|
532
|
+
credentials: 'include',
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (response.status === 401) {
|
|
536
|
+
// Authentication cleared, reload page to trigger browser auth prompt
|
|
537
|
+
window.location.reload();
|
|
538
|
+
} else {
|
|
539
|
+
console.error('Logout failed');
|
|
540
|
+
alert('Logout failed. Please clear your browser cache and reload the page.');
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error('Error during logout:', error);
|
|
544
|
+
alert('Error during logout. Please clear your browser cache and reload the page.');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Initialization on page load
|
|
549
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
550
|
+
// Setup token auth form handler
|
|
551
|
+
setupTokenAuthForm();
|
|
552
|
+
|
|
553
|
+
// Initialize authentication (check if token is needed and valid)
|
|
554
|
+
const authOk = await initializeAuth();
|
|
555
|
+
|
|
556
|
+
if (authOk) {
|
|
557
|
+
// Load auth status and form only if authenticated
|
|
558
|
+
loadAuthStatus();
|
|
559
|
+
initializeForm();
|
|
560
|
+
}
|
|
561
|
+
});
|