@ao_zorin/zocket 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/zocket-setup.cjs +12 -0
- package/bin/zocket.cjs +174 -0
- package/docs/AI_AUTODEPLOY.md +52 -0
- package/docs/CLIENTS_MCP.md +59 -0
- package/docs/INSTALL.md +288 -0
- package/docs/LOCAL_MODELS.md +95 -0
- package/package.json +52 -0
- package/pyproject.toml +29 -0
- package/scripts/ai-autodeploy.py +127 -0
- package/scripts/install-zocket.ps1 +116 -0
- package/scripts/install-zocket.sh +228 -0
- package/zocket/__init__.py +2 -0
- package/zocket/__main__.py +5 -0
- package/zocket/audit.py +76 -0
- package/zocket/auth.py +34 -0
- package/zocket/autostart.py +281 -0
- package/zocket/backup.py +33 -0
- package/zocket/cli.py +655 -0
- package/zocket/config_store.py +68 -0
- package/zocket/crypto.py +158 -0
- package/zocket/harden.py +136 -0
- package/zocket/i18n.py +216 -0
- package/zocket/mcp_server.py +249 -0
- package/zocket/paths.py +50 -0
- package/zocket/runner.py +108 -0
- package/zocket/templates/index.html +1062 -0
- package/zocket/templates/login.html +244 -0
- package/zocket/vault.py +331 -0
- package/zocket/web.py +490 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{ lang }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>zocket</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--primary-color: #0088cc;
|
|
10
|
+
--primary-accent: #00a1ff;
|
|
11
|
+
--primary-dark: #006699;
|
|
12
|
+
--primary-light: rgba(0, 136, 204, 0.12);
|
|
13
|
+
--light-bg: #ffffff;
|
|
14
|
+
--light-surface: #f8f9fa;
|
|
15
|
+
--light-text: #2c2c2c;
|
|
16
|
+
--light-muted: #6c757d;
|
|
17
|
+
--dark-bg: #18222d;
|
|
18
|
+
--dark-surface: #212e3c;
|
|
19
|
+
--dark-text: #e9ecef;
|
|
20
|
+
--dark-muted: #adb5bd;
|
|
21
|
+
--border-radius: 14px;
|
|
22
|
+
--transition: all 0.35s cubic-bezier(0.165, 0.84, 0.44, 1);
|
|
23
|
+
--shadow-light: 0 10px 30px rgba(0, 0, 0, 0.08);
|
|
24
|
+
--shadow-dark: 0 20px 45px rgba(0, 0, 0, 0.25);
|
|
25
|
+
--page-gradient: radial-gradient(circle at 0 0, #e2e8f0 0, #f8fafc 45%);
|
|
26
|
+
--bg: var(--light-bg);
|
|
27
|
+
--text: var(--light-text);
|
|
28
|
+
--muted: var(--light-muted);
|
|
29
|
+
--border: #cbd5e1;
|
|
30
|
+
--card: var(--light-surface);
|
|
31
|
+
--danger: #ff6b6b;
|
|
32
|
+
}
|
|
33
|
+
* {
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
}
|
|
36
|
+
body {
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 24px;
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
background: var(--page-gradient);
|
|
41
|
+
color: var(--text);
|
|
42
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
line-height: 1.6;
|
|
45
|
+
}
|
|
46
|
+
body[data-theme="zorin"] {
|
|
47
|
+
--bg: var(--dark-bg);
|
|
48
|
+
--text: var(--dark-text);
|
|
49
|
+
--muted: var(--dark-muted);
|
|
50
|
+
--border: rgba(255, 255, 255, 0.15);
|
|
51
|
+
--card: rgba(24, 34, 45, 0.9);
|
|
52
|
+
--page-gradient: radial-gradient(circle at 15% -10%, rgba(0, 136, 204, 0.4), rgba(8, 16, 30, 0.95) 45%), radial-gradient(circle at 80% 20%, rgba(0, 136, 204, 0.18), rgba(8, 11, 24, 0) 70%), linear-gradient(180deg, #18222d 0%, #0f172a 60%, #0b1a2b 100%);
|
|
53
|
+
}
|
|
54
|
+
body[data-theme="zorin"][data-theme-variant="light"] {
|
|
55
|
+
--bg: var(--light-bg);
|
|
56
|
+
--text: var(--light-text);
|
|
57
|
+
--muted: var(--light-muted);
|
|
58
|
+
--border: rgba(0, 136, 204, 0.25);
|
|
59
|
+
--card: rgba(255, 255, 255, 0.95);
|
|
60
|
+
--page-gradient: radial-gradient(circle at 25% -20%, rgba(0, 136, 204, 0.25), transparent 45%), radial-gradient(circle at 80% 5%, rgba(0, 136, 204, 0.1), transparent 70%), linear-gradient(180deg, #ffffff 0%, #d7efff 100%);
|
|
61
|
+
}
|
|
62
|
+
h1, h2, h3 {
|
|
63
|
+
margin: 0 0 10px;
|
|
64
|
+
}
|
|
65
|
+
p {
|
|
66
|
+
margin: 0 0 12px;
|
|
67
|
+
color: var(--muted);
|
|
68
|
+
}
|
|
69
|
+
.layout {
|
|
70
|
+
display: grid;
|
|
71
|
+
gap: 16px;
|
|
72
|
+
grid-template-columns: 320px 1fr;
|
|
73
|
+
background: rgba(255, 255, 255, 0.7);
|
|
74
|
+
border-radius: 30px;
|
|
75
|
+
}
|
|
76
|
+
body[data-theme="zorin"] .layout {
|
|
77
|
+
background: rgba(7, 11, 32, 0.7);
|
|
78
|
+
border: 1px solid rgba(124, 230, 255, 0.3);
|
|
79
|
+
padding: 32px;
|
|
80
|
+
border-radius: 32px;
|
|
81
|
+
box-shadow: var(--shadow-dark);
|
|
82
|
+
backdrop-filter: blur(32px);
|
|
83
|
+
}
|
|
84
|
+
body[data-theme="zorin"][data-theme-variant="light"] .layout {
|
|
85
|
+
background: rgba(255, 255, 255, 0.96);
|
|
86
|
+
border: 1px solid rgba(0, 136, 204, 0.2);
|
|
87
|
+
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.15);
|
|
88
|
+
}
|
|
89
|
+
.card {
|
|
90
|
+
border-radius: var(--border-radius);
|
|
91
|
+
background: var(--card);
|
|
92
|
+
border: 1px solid var(--border);
|
|
93
|
+
padding: 22px;
|
|
94
|
+
box-shadow: var(--shadow-light);
|
|
95
|
+
transition: var(--transition);
|
|
96
|
+
}
|
|
97
|
+
.card:hover {
|
|
98
|
+
transform: translateY(-3px);
|
|
99
|
+
}
|
|
100
|
+
.gradient-text {
|
|
101
|
+
background: linear-gradient(90deg, var(--primary-color), var(--primary-accent));
|
|
102
|
+
-webkit-background-clip: text;
|
|
103
|
+
background-clip: text;
|
|
104
|
+
color: transparent;
|
|
105
|
+
display: inline-block;
|
|
106
|
+
}
|
|
107
|
+
table {
|
|
108
|
+
width: 100%;
|
|
109
|
+
border-collapse: collapse;
|
|
110
|
+
}
|
|
111
|
+
th, td {
|
|
112
|
+
text-align: left;
|
|
113
|
+
padding: 12px;
|
|
114
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
115
|
+
}
|
|
116
|
+
.inline {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 8px;
|
|
120
|
+
}
|
|
121
|
+
input, button, textarea, select {
|
|
122
|
+
width: 100%;
|
|
123
|
+
padding: 12px;
|
|
124
|
+
border-radius: var(--border-radius);
|
|
125
|
+
border: 1px solid var(--border);
|
|
126
|
+
margin-bottom: 12px;
|
|
127
|
+
font: inherit;
|
|
128
|
+
background: var(--card);
|
|
129
|
+
color: var(--text);
|
|
130
|
+
transition: var(--transition);
|
|
131
|
+
}
|
|
132
|
+
input:focus,
|
|
133
|
+
textarea:focus,
|
|
134
|
+
select:focus {
|
|
135
|
+
outline: none;
|
|
136
|
+
border-color: var(--primary-color);
|
|
137
|
+
box-shadow: 0 0 0 2px rgba(0, 136, 204, 0.25);
|
|
138
|
+
}
|
|
139
|
+
button {
|
|
140
|
+
background: linear-gradient(135deg, var(--primary-color), var(--primary-accent));
|
|
141
|
+
color: white;
|
|
142
|
+
border: none;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
box-shadow: var(--shadow-light);
|
|
145
|
+
}
|
|
146
|
+
button:hover {
|
|
147
|
+
transform: translateY(-5px);
|
|
148
|
+
box-shadow: 0 15px 30px rgba(0, 136, 204, 0.35);
|
|
149
|
+
}
|
|
150
|
+
.btn-small {
|
|
151
|
+
width: auto;
|
|
152
|
+
padding: 8px 12px;
|
|
153
|
+
}
|
|
154
|
+
.danger {
|
|
155
|
+
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
|
156
|
+
}
|
|
157
|
+
.theme-inline {
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 6px;
|
|
161
|
+
}
|
|
162
|
+
.styled-select {
|
|
163
|
+
appearance: none;
|
|
164
|
+
-webkit-appearance: none;
|
|
165
|
+
-moz-appearance: none;
|
|
166
|
+
padding: 10px 28px 10px 16px;
|
|
167
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0)) border-box, var(--card);
|
|
168
|
+
border-radius: 999px;
|
|
169
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
170
|
+
color: var(--text);
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
background-position: right 10px center, center;
|
|
174
|
+
background-size: 12px auto, 100% 100%;
|
|
175
|
+
background-repeat: no-repeat, no-repeat;
|
|
176
|
+
background-image: url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"%3E%3Cpolyline points=\"6 8 10 12 14 8\"/%3E%3C/svg%3E'), none;
|
|
177
|
+
}
|
|
178
|
+
.styled-select:focus {
|
|
179
|
+
outline: none;
|
|
180
|
+
box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.2);
|
|
181
|
+
}
|
|
182
|
+
.variant-toggle {
|
|
183
|
+
display: none;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 8px;
|
|
186
|
+
margin-left: 6px;
|
|
187
|
+
}
|
|
188
|
+
body[data-theme="zorin"] .variant-toggle {
|
|
189
|
+
display: inline-flex;
|
|
190
|
+
}
|
|
191
|
+
.variant-button {
|
|
192
|
+
width: 40px;
|
|
193
|
+
height: 40px;
|
|
194
|
+
border-radius: 50%;
|
|
195
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
196
|
+
background: rgba(255, 255, 255, 0.08);
|
|
197
|
+
color: var(--primary-color);
|
|
198
|
+
font-size: 1.1rem;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
transition: var(--transition);
|
|
201
|
+
}
|
|
202
|
+
.variant-button.active {
|
|
203
|
+
background: var(--primary-color);
|
|
204
|
+
color: #fff;
|
|
205
|
+
box-shadow: 0 0 12px rgba(0, 136, 204, 0.6);
|
|
206
|
+
}
|
|
207
|
+
.theme-icon {
|
|
208
|
+
width: 18px;
|
|
209
|
+
height: 18px;
|
|
210
|
+
display: block;
|
|
211
|
+
}
|
|
212
|
+
.topline {
|
|
213
|
+
display: flex;
|
|
214
|
+
justify-content: space-between;
|
|
215
|
+
gap: 12px;
|
|
216
|
+
align-items: center;
|
|
217
|
+
margin-bottom: 18px;
|
|
218
|
+
}
|
|
219
|
+
.topline a,
|
|
220
|
+
.topline a:visited,
|
|
221
|
+
.topline a:active {
|
|
222
|
+
color: var(--primary-color);
|
|
223
|
+
}
|
|
224
|
+
a,
|
|
225
|
+
a:visited,
|
|
226
|
+
a:active,
|
|
227
|
+
a:hover,
|
|
228
|
+
a:focus {
|
|
229
|
+
color: var(--primary-color);
|
|
230
|
+
}
|
|
231
|
+
body[data-theme="zorin"] .topline {
|
|
232
|
+
background: rgba(4, 8, 25, 0.6);
|
|
233
|
+
padding: 16px 20px;
|
|
234
|
+
border-radius: 16px;
|
|
235
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
236
|
+
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4);
|
|
237
|
+
align-items: center;
|
|
238
|
+
}
|
|
239
|
+
body[data-theme="zorin"][data-theme-variant="light"] .topline {
|
|
240
|
+
background: rgba(255, 255, 255, 0.92);
|
|
241
|
+
border: 1px solid rgba(0, 136, 204, 0.35);
|
|
242
|
+
box-shadow: 0 12px 25px rgba(15, 25, 60, 0.1);
|
|
243
|
+
}
|
|
244
|
+
.mono {
|
|
245
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
246
|
+
}
|
|
247
|
+
.folder-field,
|
|
248
|
+
.folder-actions-inline,
|
|
249
|
+
.secret-preset-row {
|
|
250
|
+
width: 100%;
|
|
251
|
+
}
|
|
252
|
+
.folder-field {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
gap: 8px;
|
|
256
|
+
}
|
|
257
|
+
.folder-panel {
|
|
258
|
+
border-radius: 22px;
|
|
259
|
+
padding: 18px 22px;
|
|
260
|
+
background: linear-gradient(135deg, rgba(0, 136, 204, 0.15), rgba(255, 255, 255, 0.05)), var(--card);
|
|
261
|
+
border: 1px solid rgba(0, 136, 204, 0.25);
|
|
262
|
+
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.15);
|
|
263
|
+
position: relative;
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
}
|
|
266
|
+
.folder-panel::after {
|
|
267
|
+
content: "";
|
|
268
|
+
position: absolute;
|
|
269
|
+
inset: 0;
|
|
270
|
+
background: radial-gradient(circle at top right, rgba(0, 136, 204, 0.3), transparent 60%);
|
|
271
|
+
opacity: 0.4;
|
|
272
|
+
pointer-events: none;
|
|
273
|
+
}
|
|
274
|
+
.folder-panel__note {
|
|
275
|
+
margin: 0;
|
|
276
|
+
font-size: 0.9rem;
|
|
277
|
+
color: var(--muted);
|
|
278
|
+
}
|
|
279
|
+
body[data-theme="zorin"] .folder-panel {
|
|
280
|
+
background: rgba(8, 11, 24, 0.6);
|
|
281
|
+
border-color: rgba(255, 255, 255, 0.2);
|
|
282
|
+
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.65);
|
|
283
|
+
}
|
|
284
|
+
body[data-theme="zorin"][data-theme-variant="light"] .folder-panel {
|
|
285
|
+
background: rgba(255, 255, 255, 0.95);
|
|
286
|
+
border-color: rgba(0, 136, 204, 0.25);
|
|
287
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
|
288
|
+
}
|
|
289
|
+
body[data-theme="zorin"] .folder-panel__note {
|
|
290
|
+
color: var(--danger);
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
letter-spacing: 0.02em;
|
|
293
|
+
}
|
|
294
|
+
.folder-actions-inline {
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-wrap: wrap;
|
|
297
|
+
gap: 8px;
|
|
298
|
+
align-items: center;
|
|
299
|
+
}
|
|
300
|
+
.folder-actions-inline button {
|
|
301
|
+
width: auto;
|
|
302
|
+
min-width: 120px;
|
|
303
|
+
}
|
|
304
|
+
.folder-actions-inline input {
|
|
305
|
+
flex: 1 1 320px;
|
|
306
|
+
}
|
|
307
|
+
.folder-list {
|
|
308
|
+
max-height: 260px;
|
|
309
|
+
overflow: auto;
|
|
310
|
+
border: 1px solid var(--border);
|
|
311
|
+
border-radius: var(--border-radius);
|
|
312
|
+
padding: 8px;
|
|
313
|
+
}
|
|
314
|
+
.folder-search {
|
|
315
|
+
margin-top: 8px;
|
|
316
|
+
}
|
|
317
|
+
.secret-preset-row select {
|
|
318
|
+
min-width: 260px;
|
|
319
|
+
}
|
|
320
|
+
.secret-preset-group {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-wrap: wrap;
|
|
323
|
+
gap: 12px;
|
|
324
|
+
}
|
|
325
|
+
.secret-preset-group > * {
|
|
326
|
+
flex: 1 1 220px;
|
|
327
|
+
min-width: 180px;
|
|
328
|
+
}
|
|
329
|
+
.secret-preset-group[hidden] {
|
|
330
|
+
display: none;
|
|
331
|
+
}
|
|
332
|
+
.secret-actions,
|
|
333
|
+
.secret-list-actions {
|
|
334
|
+
display: flex;
|
|
335
|
+
gap: 10px;
|
|
336
|
+
flex-wrap: nowrap;
|
|
337
|
+
align-items: center;
|
|
338
|
+
}
|
|
339
|
+
.secret-actions form {
|
|
340
|
+
margin: 0;
|
|
341
|
+
}
|
|
342
|
+
.secret-actions button,
|
|
343
|
+
.secret-list-actions button {
|
|
344
|
+
width: auto;
|
|
345
|
+
white-space: nowrap;
|
|
346
|
+
}
|
|
347
|
+
.modal {
|
|
348
|
+
position: fixed;
|
|
349
|
+
inset: 0;
|
|
350
|
+
background: rgba(7, 11, 32, 0.85);
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
justify-content: center;
|
|
354
|
+
padding: 16px;
|
|
355
|
+
}
|
|
356
|
+
.modal[hidden] {
|
|
357
|
+
display: none;
|
|
358
|
+
}
|
|
359
|
+
.modal-card {
|
|
360
|
+
width: min(780px, 96vw);
|
|
361
|
+
max-height: 90vh;
|
|
362
|
+
overflow: auto;
|
|
363
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
364
|
+
border-radius: var(--border-radius);
|
|
365
|
+
background: var(--card);
|
|
366
|
+
padding: 20px;
|
|
367
|
+
box-shadow: var(--shadow-dark);
|
|
368
|
+
backdrop-filter: blur(24px);
|
|
369
|
+
}
|
|
370
|
+
.notice {
|
|
371
|
+
border: 1px solid rgba(0, 136, 204, 0.3);
|
|
372
|
+
background: rgba(0, 136, 204, 0.08);
|
|
373
|
+
color: var(--primary-dark);
|
|
374
|
+
}
|
|
375
|
+
.error {
|
|
376
|
+
border: 1px solid #ffe0e6;
|
|
377
|
+
background: #fff2f5;
|
|
378
|
+
color: #9f1239;
|
|
379
|
+
border-radius: 10px;
|
|
380
|
+
padding: 12px;
|
|
381
|
+
}
|
|
382
|
+
body[data-theme="zorin"] .error {
|
|
383
|
+
background: rgba(248, 113, 113, 0.12);
|
|
384
|
+
border-color: rgba(248, 113, 113, 0.4);
|
|
385
|
+
color: #ffe4e6;
|
|
386
|
+
box-shadow: 0 0 12px rgba(248, 113, 113, 0.25);
|
|
387
|
+
}
|
|
388
|
+
body[data-theme="zorin"][data-theme-variant="light"] .error {
|
|
389
|
+
background: rgba(248, 113, 113, 0.2);
|
|
390
|
+
border-color: rgba(248, 113, 113, 0.6);
|
|
391
|
+
color: #991b1b;
|
|
392
|
+
}
|
|
393
|
+
@media (max-width: 980px) {
|
|
394
|
+
.layout {
|
|
395
|
+
grid-template-columns: 1fr;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
</style>
|
|
399
|
+
</head>
|
|
400
|
+
<body data-theme="{{ theme }}" data-theme-variant="{{ theme_variant }}">
|
|
401
|
+
<div class="topline">
|
|
402
|
+
<div>
|
|
403
|
+
<h1>zocket</h1>
|
|
404
|
+
<p>{{ t("app.tagline") }}</p>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="inline">
|
|
407
|
+
<span>{{ t("ui.lang") }}:</span>
|
|
408
|
+
<a href="/?lang=en{% if selected_project %}&project={{ selected_project }}{% endif %}">EN</a>
|
|
409
|
+
<a href="/?lang=ru{% if selected_project %}&project={{ selected_project }}{% endif %}">RU</a>
|
|
410
|
+
<form method="post" action="/set-theme" class="theme-inline">
|
|
411
|
+
<label for="main-theme-select">{{ t("ui.theme") }}:</label>
|
|
412
|
+
<select id="main-theme-select" class="styled-select" name="theme" onchange="this.form.submit();">
|
|
413
|
+
<option value="standard" {% if theme == "standard" %}selected{% endif %}>{{ t("ui.theme_standard") }}</option>
|
|
414
|
+
<option value="zorin" {% if theme == "zorin" %}selected{% endif %}>{{ t("ui.theme_zorin") }}</option>
|
|
415
|
+
</select>
|
|
416
|
+
<input type="hidden" name="next" value="{{ request.full_path }}" />
|
|
417
|
+
</form>
|
|
418
|
+
{% if theme == "zorin" %}
|
|
419
|
+
<form method="post" action="/set-theme-variant" class="variant-toggle">
|
|
420
|
+
<input type="hidden" name="next" value="{{ request.full_path }}" />
|
|
421
|
+
{% set next_variant = 'dark' if theme_variant == 'light' else 'light' %}
|
|
422
|
+
{% set label_key = 'ui.variant_dark' if next_variant == 'dark' else 'ui.variant_light' %}
|
|
423
|
+
<button type="submit" name="variant" value="{{ next_variant }}" class="variant-button" aria-label="{{ t(label_key) }}">
|
|
424
|
+
{% if theme_variant == 'light' %}
|
|
425
|
+
<svg class="theme-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
426
|
+
<circle cx="12" cy="12" r="5"></circle>
|
|
427
|
+
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
428
|
+
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
429
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
430
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
431
|
+
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
432
|
+
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
433
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
434
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
435
|
+
</svg>
|
|
436
|
+
{% else %}
|
|
437
|
+
<svg class="theme-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
438
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
439
|
+
</svg>
|
|
440
|
+
{% endif %}
|
|
441
|
+
</button>
|
|
442
|
+
</form>
|
|
443
|
+
{% endif %}
|
|
444
|
+
<form method="post" action="/logout">
|
|
445
|
+
<button type="submit">{{ t("ui.logout") }}</button>
|
|
446
|
+
</form>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
{% if error %}
|
|
450
|
+
<div class="error">{{ error }}</div>
|
|
451
|
+
{% endif %}
|
|
452
|
+
{% if generated_password %}
|
|
453
|
+
<div class="notice">
|
|
454
|
+
{{ t("ui.generated_password_notice") }}
|
|
455
|
+
<div class="mono">{{ generated_password }}</div>
|
|
456
|
+
<div>{{ t("ui.generated_password_save_now") }}</div>
|
|
457
|
+
</div>
|
|
458
|
+
{% endif %}
|
|
459
|
+
|
|
460
|
+
<div class="layout">
|
|
461
|
+
<aside class="card">
|
|
462
|
+
<h2>{{ t("ui.projects") }}</h2>
|
|
463
|
+
<table>
|
|
464
|
+
<thead>
|
|
465
|
+
<tr>
|
|
466
|
+
<th>{{ t("ui.name") }}</th>
|
|
467
|
+
<th>{{ t("ui.keys_count") }}</th>
|
|
468
|
+
</tr>
|
|
469
|
+
</thead>
|
|
470
|
+
<tbody>
|
|
471
|
+
{% for project in projects %}
|
|
472
|
+
<tr>
|
|
473
|
+
<td><a href="/?project={{ project.project }}">{{ project.project }}</a></td>
|
|
474
|
+
<td>{{ project.secret_count }}</td>
|
|
475
|
+
</tr>
|
|
476
|
+
{% endfor %}
|
|
477
|
+
</tbody>
|
|
478
|
+
</table>
|
|
479
|
+
<hr />
|
|
480
|
+
<h3>{{ t("ui.new_project") }}</h3>
|
|
481
|
+
<form method="post" action="/projects/create">
|
|
482
|
+
<input name="name" placeholder="project-name" required />
|
|
483
|
+
<input name="description" placeholder="{{ t('ui.optional_desc') }}" />
|
|
484
|
+
<div class="folder-field create-gap">
|
|
485
|
+
<input id="create-folder-path" name="folder_path" placeholder="{{ t('ui.optional_folder') }}" />
|
|
486
|
+
<button class="btn-small" type="button" onclick="openFolderPicker('create-folder-path')">{{ t("ui.choose_folder") }}</button>
|
|
487
|
+
</div>
|
|
488
|
+
<button type="submit">{{ t("ui.create") }}</button>
|
|
489
|
+
</form>
|
|
490
|
+
</aside>
|
|
491
|
+
|
|
492
|
+
<main class="card">
|
|
493
|
+
{% if selected_project %}
|
|
494
|
+
<h2>{{ selected_project }}</h2>
|
|
495
|
+
<p>
|
|
496
|
+
{{ t("ui.project_folder") }}:
|
|
497
|
+
{% if selected_project_info and selected_project_info.folder_path %}
|
|
498
|
+
<span class="mono">{{ selected_project_info.folder_path }}</span>
|
|
499
|
+
{% else %}
|
|
500
|
+
{{ t("ui.not_set") }}
|
|
501
|
+
{% endif %}
|
|
502
|
+
</p>
|
|
503
|
+
<div class="folder-panel">
|
|
504
|
+
<form method="post" action="/projects/{{ selected_project }}/folder" class="folder-actions-inline">
|
|
505
|
+
<input id="edit-folder-path" name="folder_path" placeholder="{{ t('ui.optional_folder') }}" value="{{ selected_project_info.folder_path if selected_project_info and selected_project_info.folder_path else '' }}" />
|
|
506
|
+
<button class="btn-small" type="button" onclick="openFolderPicker('edit-folder-path')">{{ t("ui.choose_folder") }}</button>
|
|
507
|
+
<button class="btn-small" type="submit">{{ t("ui.save_folder") }}</button>
|
|
508
|
+
<button class="btn-small danger" type="submit" name="clear" value="1" onclick="return confirm('{{ t('ui.clear_folder') }}?')">{{ t("ui.clear_folder") }}</button>
|
|
509
|
+
</form>
|
|
510
|
+
<p class="folder-panel__note">
|
|
511
|
+
{% if show_values %}
|
|
512
|
+
{{ t("ui.real_values_visible") }}
|
|
513
|
+
<a href="/?project={{ selected_project }}">{{ t("ui.hide_values") }}</a>
|
|
514
|
+
{% else %}
|
|
515
|
+
{{ t("ui.masked_values_visible") }}
|
|
516
|
+
<a href="/?project={{ selected_project }}&show_values=1">{{ t("ui.show_values") }}</a>
|
|
517
|
+
{% endif %}
|
|
518
|
+
</p>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<div class="inline secret-list-actions">
|
|
522
|
+
<form method="post" action="/projects/{{ selected_project }}/delete" onsubmit="return confirm('{{ t('ui.delete_project') }}?')">
|
|
523
|
+
<button class="danger" type="submit">{{ t("ui.delete_project") }}</button>
|
|
524
|
+
</form>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<h3>{{ t("ui.secrets") }}</h3>
|
|
528
|
+
<table id="secrets-table">
|
|
529
|
+
<thead>
|
|
530
|
+
<tr>
|
|
531
|
+
<th>KEY</th>
|
|
532
|
+
<th>{{ t("ui.value") }}</th>
|
|
533
|
+
<th>{{ t("ui.description") }}</th>
|
|
534
|
+
<th>{{ t("ui.updated_at") }}</th>
|
|
535
|
+
<th></th>
|
|
536
|
+
</tr>
|
|
537
|
+
</thead>
|
|
538
|
+
<tbody>
|
|
539
|
+
{% for s in secrets %}
|
|
540
|
+
<tr data-secret-key="{{ s.key }}">
|
|
541
|
+
<td class="mono">{{ s.key }}</td>
|
|
542
|
+
<td class="mono">
|
|
543
|
+
{% if show_values %}
|
|
544
|
+
{{ s.value }}
|
|
545
|
+
{% else %}
|
|
546
|
+
***
|
|
547
|
+
{% endif %}
|
|
548
|
+
</td>
|
|
549
|
+
<td>{{ s.description }}</td>
|
|
550
|
+
<td>{{ s.updated_at }}</td>
|
|
551
|
+
<td>
|
|
552
|
+
<div class="secret-actions">
|
|
553
|
+
<button type="button" class="btn-small" data-edit-secret="{{ s.key }}">{{ t("ui.edit_secret") }}</button>
|
|
554
|
+
<form method="post" action="/projects/{{ selected_project }}/secrets/{{ s.key }}/delete" onsubmit="return confirm('{{ t('ui.delete') }} {{ s.key }}?')">
|
|
555
|
+
<button class="danger" type="submit">{{ t("ui.delete") }}</button>
|
|
556
|
+
</form>
|
|
557
|
+
</div>
|
|
558
|
+
</td>
|
|
559
|
+
</tr>
|
|
560
|
+
{% endfor %}
|
|
561
|
+
</tbody>
|
|
562
|
+
</table>
|
|
563
|
+
|
|
564
|
+
<h3>{{ t("ui.add_or_update_secret") }}</h3>
|
|
565
|
+
<form method="post" action="/projects/{{ selected_project }}/secrets/upsert" id="secret-form">
|
|
566
|
+
<div class="secret-preset-row">
|
|
567
|
+
<span>{{ t("ui.secret_preset") }}</span>
|
|
568
|
+
<select id="secret-preset" name="preset">
|
|
569
|
+
<option value="" disabled selected>{{ t('ui.choose_preset') }}</option>
|
|
570
|
+
<option value="custom">Custom key/value</option>
|
|
571
|
+
<option value="ssh">SSH credentials</option>
|
|
572
|
+
<option value="account">Account login/password</option>
|
|
573
|
+
<option value="email">Email</option>
|
|
574
|
+
<option value="api">API credentials</option>
|
|
575
|
+
<option value="db">Database credentials</option>
|
|
576
|
+
<option value="oauth">OAuth 2.0</option>
|
|
577
|
+
<option value="tls">TLS certificate</option>
|
|
578
|
+
<option value="webhook">Webhook</option>
|
|
579
|
+
<option value="ftp">FTP access</option>
|
|
580
|
+
<option value="ftps">FTPS access</option>
|
|
581
|
+
<option value="sftp">SFTP access</option>
|
|
582
|
+
<option value="smtp">SMTP credentials</option>
|
|
583
|
+
<option value="imap">IMAP credentials</option>
|
|
584
|
+
<option value="pop3">POP3 credentials</option>
|
|
585
|
+
</select>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="secret-preset-groups">
|
|
588
|
+
<input type="hidden" id="secret-key" name="key" />
|
|
589
|
+
<input type="hidden" id="secret-value-hidden" name="value" />
|
|
590
|
+
<div class="secret-preset-group" data-secret-preset="custom">
|
|
591
|
+
<input id="custom-name" name="custom_name" placeholder="{{ t('ui.friendly_name') }}" />
|
|
592
|
+
<input id="custom-key" name="custom_key" placeholder="{{ t('ui.key') }}" required />
|
|
593
|
+
<input id="custom-value" name="custom_value" placeholder="{{ t('ui.value') }}" required />
|
|
594
|
+
</div>
|
|
595
|
+
<div class="secret-preset-group" data-secret-preset="ssh" hidden>
|
|
596
|
+
<input id="ssh-user" name="ssh_user" placeholder="SSH user" />
|
|
597
|
+
<input id="ssh-host" name="ssh_host" placeholder="SSH host" />
|
|
598
|
+
<input id="ssh-port" name="ssh_port" placeholder="SSH port" value="21" />
|
|
599
|
+
<input id="ssh-password" type="password" name="ssh_password" placeholder="Key" />
|
|
600
|
+
</div>
|
|
601
|
+
<div class="secret-preset-group" data-secret-preset="account" hidden>
|
|
602
|
+
<input id="account-login" name="account_login" placeholder="Account login" />
|
|
603
|
+
<input id="account-password" type="password" name="account_password" placeholder="Account password" />
|
|
604
|
+
</div>
|
|
605
|
+
<div class="secret-preset-group" data-secret-preset="email" hidden>
|
|
606
|
+
<input id="email-value" type="email" name="email_value" placeholder="Email" />
|
|
607
|
+
</div>
|
|
608
|
+
<div class="secret-preset-group" data-secret-preset="api" hidden>
|
|
609
|
+
<input id="api-platform" name="api_platform" placeholder="Platform name" />
|
|
610
|
+
<input id="api-key" type="password" name="api_key" placeholder="API key" />
|
|
611
|
+
<input id="api-base-url" name="api_base_url" placeholder="Base URL (optional)" />
|
|
612
|
+
<select id="api-auth-type" name="api_auth_type">
|
|
613
|
+
<option value="">Auth type (optional)</option>
|
|
614
|
+
<option value="BEARER">Bearer</option>
|
|
615
|
+
<option value="BASIC">Basic</option>
|
|
616
|
+
<option value="TOKEN">Token</option>
|
|
617
|
+
<option value="CUSTOM">Custom</option>
|
|
618
|
+
</select>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="secret-preset-group" data-secret-preset="db" hidden>
|
|
621
|
+
<input id="db-host" name="db_host" placeholder="DB host" />
|
|
622
|
+
<input id="db-port" name="db_port" placeholder="DB port" value="5432" />
|
|
623
|
+
<input id="db-user" name="db_user" placeholder="DB user" />
|
|
624
|
+
<input id="db-password" name="db_password" placeholder="DB password" type="password" />
|
|
625
|
+
<input id="db-name" name="db_name" placeholder="DB name" />
|
|
626
|
+
<input id="db-driver" name="db_driver" placeholder="Driver prefix (optional)" />
|
|
627
|
+
</div>
|
|
628
|
+
<div class="secret-preset-group" data-secret-preset="oauth" hidden>
|
|
629
|
+
<input id="oauth-client-id" name="oauth_client_id" placeholder="Client ID" />
|
|
630
|
+
<input id="oauth-client-secret" name="oauth_client_secret" placeholder="Client secret" type="password" />
|
|
631
|
+
<input id="oauth-redirect" name="oauth_redirect" placeholder="Redirect URI (optional)" />
|
|
632
|
+
<input id="oauth-scope" name="oauth_scope" placeholder="Scope (optional)" />
|
|
633
|
+
</div>
|
|
634
|
+
<div class="secret-preset-group" data-secret-preset="tls" hidden>
|
|
635
|
+
<textarea id="tls-cert" name="tls_cert" placeholder="TLS cert"></textarea>
|
|
636
|
+
<textarea id="tls-key" name="tls_key" placeholder="TLS key"></textarea>
|
|
637
|
+
<input id="tls-passphrase" name="tls_passphrase" placeholder="TLS passphrase (optional)" type="password" />
|
|
638
|
+
</div>
|
|
639
|
+
<div class="secret-preset-group" data-secret-preset="webhook" hidden>
|
|
640
|
+
<input id="webhook-url" name="webhook_url" placeholder="Webhook URL" />
|
|
641
|
+
<input id="webhook-secret" name="webhook_secret" placeholder="Webhook secret (optional)" type="password" />
|
|
642
|
+
<input id="webhook-method" name="webhook_method" placeholder="HTTP method (optional)" />
|
|
643
|
+
</div>
|
|
644
|
+
<div class="secret-preset-group" data-secret-preset="ftp" hidden>
|
|
645
|
+
<input id="ftp-host" name="ftp_host" placeholder="Host" />
|
|
646
|
+
<input id="ftp-user" name="ftp_user" placeholder="User" />
|
|
647
|
+
<input id="ftp-port" name="ftp_port" placeholder="Port" value="21" />
|
|
648
|
+
<input id="ftp-password" name="ftp_password" placeholder="Password" type="password" />
|
|
649
|
+
</div>
|
|
650
|
+
<div class="secret-preset-group" data-secret-preset="ftps" hidden>
|
|
651
|
+
<input id="ftps-host" name="ftps_host" placeholder="Host" />
|
|
652
|
+
<input id="ftps-user" name="ftps_user" placeholder="User" />
|
|
653
|
+
<input id="ftps-port" name="ftps_port" placeholder="Port" value="990" />
|
|
654
|
+
<input id="ftps-password" name="ftps_password" placeholder="Password" type="password" />
|
|
655
|
+
</div>
|
|
656
|
+
<div class="secret-preset-group" data-secret-preset="sftp" hidden>
|
|
657
|
+
<input id="sftp-host" name="sftp_host" placeholder="Host" />
|
|
658
|
+
<input id="sftp-user" name="sftp_user" placeholder="User" />
|
|
659
|
+
<input id="sftp-port" name="sftp_port" placeholder="Port" value="22" />
|
|
660
|
+
<input id="sftp-password" name="sftp_password" placeholder="Password" type="password" />
|
|
661
|
+
</div>
|
|
662
|
+
<div class="secret-preset-group" data-secret-preset="smtp" hidden>
|
|
663
|
+
<input id="smtp-host" name="smtp_host" placeholder="Host" />
|
|
664
|
+
<input id="smtp-user" name="smtp_user" placeholder="User" />
|
|
665
|
+
<input id="smtp-port" name="smtp_port" placeholder="Port" value="587" />
|
|
666
|
+
<input id="smtp-password" name="smtp_password" placeholder="Password" type="password" />
|
|
667
|
+
</div>
|
|
668
|
+
<div class="secret-preset-group" data-secret-preset="imap" hidden>
|
|
669
|
+
<input id="imap-host" name="imap_host" placeholder="Host" />
|
|
670
|
+
<input id="imap-user" name="imap_user" placeholder="User" />
|
|
671
|
+
<input id="imap-port" name="imap_port" placeholder="Port" value="993" />
|
|
672
|
+
<input id="imap-password" name="imap_password" placeholder="Password" type="password" />
|
|
673
|
+
</div>
|
|
674
|
+
<div class="secret-preset-group" data-secret-preset="pop3" hidden>
|
|
675
|
+
<input id="pop3-host" name="pop3_host" placeholder="Host" />
|
|
676
|
+
<input id="pop3-user" name="pop3_user" placeholder="User" />
|
|
677
|
+
<input id="pop3-port" name="pop3_port" placeholder="Port" value="995" />
|
|
678
|
+
<input id="pop3-password" name="pop3_password" placeholder="Password" type="password" />
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
<input name="description" placeholder="{{ t('ui.optional_desc') }}" />
|
|
682
|
+
<button type="submit">{{ t("ui.save") }}</button>
|
|
683
|
+
</form>
|
|
684
|
+
{% else %}
|
|
685
|
+
<h2>{{ t("ui.no_projects") }}</h2>
|
|
686
|
+
<p>{{ t("ui.create_left") }}</p>
|
|
687
|
+
{% endif %}
|
|
688
|
+
</main>
|
|
689
|
+
</div>
|
|
690
|
+
|
|
691
|
+
<div id="folder-picker-modal" class="modal" hidden>
|
|
692
|
+
<div class="modal-card">
|
|
693
|
+
<div class="modal-head">
|
|
694
|
+
<h3>{{ t("ui.folder_picker") }}</h3>
|
|
695
|
+
<button class="btn-small danger" type="button" onclick="closeFolderPicker()">{{ t("ui.close") }}</button>
|
|
696
|
+
</div>
|
|
697
|
+
<p>{{ t("ui.current_path") }}: <span class="mono" id="folder-picker-current">-</span></p>
|
|
698
|
+
<div class="inline">
|
|
699
|
+
<button class="btn-small" type="button" id="folder-picker-up" onclick="folderPickerGoParent()">{{ t("ui.parent_folder") }}</button>
|
|
700
|
+
<button class="btn-small" type="button" onclick="folderPickerGoRoots()">{{ t("ui.roots") }}</button>
|
|
701
|
+
<button class="btn-small" type="button" id="folder-picker-select" onclick="folderPickerSelectCurrent()">{{ t("ui.select_folder") }}</button>
|
|
702
|
+
</div>
|
|
703
|
+
<input id="folder-picker-search" class="folder-search" type="text" placeholder="{{ t('ui.folder_search') }}" autocomplete="off" />
|
|
704
|
+
<div id="folder-picker-error" class="error" style="display:none"></div>
|
|
705
|
+
<div id="folder-picker-list" class="folder-list"></div>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
|
|
709
|
+
<script>
|
|
710
|
+
const folderPickerModal = document.getElementById("folder-picker-modal");
|
|
711
|
+
const folderPickerCurrent = document.getElementById("folder-picker-current");
|
|
712
|
+
const folderPickerList = document.getElementById("folder-picker-list");
|
|
713
|
+
const folderPickerError = document.getElementById("folder-picker-error");
|
|
714
|
+
const folderPickerUp = document.getElementById("folder-picker-up");
|
|
715
|
+
const folderPickerSelect = document.getElementById("folder-picker-select");
|
|
716
|
+
const folderPickerSearch = document.getElementById("folder-picker-search");
|
|
717
|
+
let folderPickerState = { targetId: null, current: null, parent: null };
|
|
718
|
+
let folderPickerDirs = [];
|
|
719
|
+
|
|
720
|
+
const noSubfoldersText = {{ t("ui.no_subfolders")|tojson }};
|
|
721
|
+
const noMatchingFoldersText = {{ t("ui.no_matching_folders")|tojson }};
|
|
722
|
+
const loadingText = {{ t("ui.loading")|tojson }};
|
|
723
|
+
|
|
724
|
+
folderPickerModal.hidden = true;
|
|
725
|
+
|
|
726
|
+
function setFolderPickerError(message) {
|
|
727
|
+
if (!message) {
|
|
728
|
+
folderPickerError.style.display = "none";
|
|
729
|
+
folderPickerError.textContent = "";
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
folderPickerError.style.display = "block";
|
|
733
|
+
folderPickerError.textContent = message;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function renderFolderButtons(rows) {
|
|
737
|
+
folderPickerList.innerHTML = "";
|
|
738
|
+
if (!rows || rows.length === 0) {
|
|
739
|
+
const empty = document.createElement("p");
|
|
740
|
+
empty.textContent = folderPickerSearch.value.trim() ? noMatchingFoldersText : noSubfoldersText;
|
|
741
|
+
folderPickerList.appendChild(empty);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
for (const row of rows) {
|
|
745
|
+
const button = document.createElement("button");
|
|
746
|
+
button.type = "button";
|
|
747
|
+
button.textContent = row.name;
|
|
748
|
+
button.addEventListener("click", () => loadFolderList(row.path));
|
|
749
|
+
folderPickerList.appendChild(button);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function applyFolderSearchFilter() {
|
|
754
|
+
const q = folderPickerSearch.value.trim().toLowerCase();
|
|
755
|
+
if (!q) {
|
|
756
|
+
renderFolderButtons(folderPickerDirs);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const filtered = folderPickerDirs.filter((row) => row.name.toLowerCase().includes(q));
|
|
760
|
+
renderFolderButtons(filtered);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function loadFolderList(path) {
|
|
764
|
+
setFolderPickerError("");
|
|
765
|
+
folderPickerList.innerHTML = `<p>${loadingText}</p>`;
|
|
766
|
+
const query = path ? `?path=${encodeURIComponent(path)}` : "";
|
|
767
|
+
const response = await fetch(`/api/folders${query}`, { credentials: "same-origin" });
|
|
768
|
+
const payload = await response.json();
|
|
769
|
+
if (!response.ok || !payload.ok) {
|
|
770
|
+
const message = payload && payload.error ? payload.error : {{ t('ui.folder_picker_failed')|tojson }};
|
|
771
|
+
setFolderPickerError(message);
|
|
772
|
+
folderPickerList.innerHTML = "";
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
folderPickerState.current = payload.current;
|
|
776
|
+
folderPickerState.parent = payload.parent;
|
|
777
|
+
folderPickerCurrent.textContent = payload.current || {{ t('ui.roots')|tojson }};
|
|
778
|
+
folderPickerUp.disabled = !payload.parent;
|
|
779
|
+
folderPickerSelect.disabled = !payload.current;
|
|
780
|
+
folderPickerDirs = payload.directories || [];
|
|
781
|
+
applyFolderSearchFilter();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function openFolderPicker(targetId) {
|
|
785
|
+
folderPickerState.targetId = targetId;
|
|
786
|
+
folderPickerModal.hidden = false;
|
|
787
|
+
folderPickerSearch.value = "";
|
|
788
|
+
const targetInput = document.getElementById(targetId);
|
|
789
|
+
const startPath = targetInput && targetInput.value ? targetInput.value : "";
|
|
790
|
+
loadFolderList(startPath);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function closeFolderPicker() {
|
|
794
|
+
folderPickerModal.hidden = true;
|
|
795
|
+
folderPickerState = { targetId: null, current: null, parent: null };
|
|
796
|
+
folderPickerDirs = [];
|
|
797
|
+
folderPickerSearch.value = "";
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
folderPickerModal.addEventListener("click", (event) => {
|
|
801
|
+
if (event.target === folderPickerModal) {
|
|
802
|
+
closeFolderPicker();
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
document.addEventListener("keydown", (event) => {
|
|
807
|
+
if (event.key === "Escape" && !folderPickerModal.hidden) {
|
|
808
|
+
closeFolderPicker();
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
function folderPickerGoParent() {
|
|
813
|
+
if (folderPickerState.parent) {
|
|
814
|
+
loadFolderList(folderPickerState.parent);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function folderPickerGoRoots() {
|
|
819
|
+
loadFolderList("");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function folderPickerSelectCurrent() {
|
|
823
|
+
if (!folderPickerState.targetId || !folderPickerState.current) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const targetInput = document.getElementById(folderPickerState.targetId);
|
|
827
|
+
if (!targetInput) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
targetInput.value = folderPickerState.current;
|
|
831
|
+
closeFolderPicker();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
folderPickerSearch.addEventListener("input", applyFolderSearchFilter);
|
|
835
|
+
|
|
836
|
+
const secretPreset = document.getElementById("secret-preset");
|
|
837
|
+
const secretPresetGroups = Array.from(document.querySelectorAll("[data-secret-preset]"));
|
|
838
|
+
const presetRequiredMap = {
|
|
839
|
+
custom: ["custom-key", "custom-value"],
|
|
840
|
+
ssh: ["ssh-user", "ssh-host", "ssh-password"],
|
|
841
|
+
account: ["account-login", "account-password"],
|
|
842
|
+
email: ["email-value"],
|
|
843
|
+
api: ["api-key"],
|
|
844
|
+
ftp: ["ftp-host", "ftp-user", "ftp-password"],
|
|
845
|
+
ftps: ["ftps-host", "ftps-user", "ftps-password"],
|
|
846
|
+
sftp: ["sftp-host", "sftp-user", "sftp-password"],
|
|
847
|
+
smtp: ["smtp-host", "smtp-user", "smtp-password"],
|
|
848
|
+
imap: ["imap-host", "imap-user", "imap-password"],
|
|
849
|
+
pop3: ["pop3-host", "pop3-user", "pop3-password"],
|
|
850
|
+
};
|
|
851
|
+
const presetPortDefaults = {
|
|
852
|
+
ssh: "21",
|
|
853
|
+
ftp: "21",
|
|
854
|
+
ftps: "990",
|
|
855
|
+
sftp: "22",
|
|
856
|
+
smtp: "587",
|
|
857
|
+
imap: "993",
|
|
858
|
+
pop3: "995",
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
function updateSecretPresetForm() {
|
|
862
|
+
if (!secretPreset) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const selected = secretPreset.value || "";
|
|
866
|
+
const activeRequired = new Set(presetRequiredMap[selected] || []);
|
|
867
|
+
for (const group of secretPresetGroups) {
|
|
868
|
+
const mode = group.getAttribute("data-secret-preset");
|
|
869
|
+
const isActive = selected && mode === selected;
|
|
870
|
+
group.hidden = !isActive;
|
|
871
|
+
const fields = group.querySelectorAll("input, select, textarea");
|
|
872
|
+
fields.forEach((field) => {
|
|
873
|
+
const id = field.getAttribute("id") || "";
|
|
874
|
+
field.required = isActive && activeRequired.has(id);
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
const portDefault = presetPortDefaults[selected];
|
|
878
|
+
if (portDefault) {
|
|
879
|
+
const portInput = document.getElementById(`${selected}-port`);
|
|
880
|
+
if (portInput && !portInput.value.trim()) {
|
|
881
|
+
portInput.value = portDefault;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
syncHiddenFields(selected);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const finalKeyInput = document.getElementById("secret-key");
|
|
888
|
+
const finalValueInput = document.getElementById("secret-value-hidden");
|
|
889
|
+
const customKeyInput = document.getElementById("custom-key");
|
|
890
|
+
const customValueInput = document.getElementById("custom-value");
|
|
891
|
+
|
|
892
|
+
const presetKeyMap = {
|
|
893
|
+
custom: "",
|
|
894
|
+
ssh: "SSH_PASSWORD",
|
|
895
|
+
account: "ACCOUNT_PASSWORD",
|
|
896
|
+
email: "EMAIL",
|
|
897
|
+
api: "API_KEY",
|
|
898
|
+
db: "DB_PASSWORD",
|
|
899
|
+
oauth: "OAUTH_TOKEN",
|
|
900
|
+
tls: "TLS_CERTIFICATE",
|
|
901
|
+
webhook: "WEBHOOK_SECRET",
|
|
902
|
+
ftp: "FTP_PASSWORD",
|
|
903
|
+
ftps: "FTPS_PASSWORD",
|
|
904
|
+
sftp: "SFTP_PASSWORD",
|
|
905
|
+
smtp: "SMTP_PASSWORD",
|
|
906
|
+
imap: "IMAP_PASSWORD",
|
|
907
|
+
pop3: "POP3_PASSWORD",
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
const presetValueFields = {
|
|
911
|
+
email: ["email-value"],
|
|
912
|
+
ssh: ["ssh-user", "ssh-host", "ssh-port", "ssh-password"],
|
|
913
|
+
account: ["account-login", "account-password"],
|
|
914
|
+
api: ["api-platform", "api-key", "api-base-url", "api-auth-type"],
|
|
915
|
+
db: ["db-host", "db-port", "db-user", "db-password", "db-name", "db-driver"],
|
|
916
|
+
oauth: ["oauth-client-id", "oauth-client-secret", "oauth-redirect", "oauth-scope"],
|
|
917
|
+
tls: ["tls-cert", "tls-key", "tls-passphrase"],
|
|
918
|
+
webhook: ["webhook-url", "webhook-secret", "webhook-method"],
|
|
919
|
+
ftp: ["ftp-host", "ftp-port", "ftp-user", "ftp-password"],
|
|
920
|
+
ftps: ["ftps-host", "ftps-port", "ftps-user", "ftps-password"],
|
|
921
|
+
sftp: ["sftp-host", "sftp-port", "sftp-user", "sftp-password"],
|
|
922
|
+
smtp: ["smtp-host", "smtp-port", "smtp-user", "smtp-password"],
|
|
923
|
+
imap: ["imap-host", "imap-port", "imap-user", "imap-password"],
|
|
924
|
+
pop3: ["pop3-host", "pop3-port", "pop3-user", "pop3-password"],
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
function buildPresetValue(preset) {
|
|
928
|
+
const fields = presetValueFields[preset];
|
|
929
|
+
if (!fields) {
|
|
930
|
+
return "";
|
|
931
|
+
}
|
|
932
|
+
const values = [];
|
|
933
|
+
for (const id of fields) {
|
|
934
|
+
const element = document.getElementById(id);
|
|
935
|
+
if (!element) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
const value = (element.value || "").toString().trim();
|
|
939
|
+
if (!value) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
values.push(value);
|
|
943
|
+
}
|
|
944
|
+
return values.join(" | ");
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function syncHiddenFields(selectedPreset) {
|
|
948
|
+
const preset = selectedPreset || (secretPreset ? secretPreset.value : "") || "";
|
|
949
|
+
if (preset === "custom") {
|
|
950
|
+
if (customKeyInput && finalKeyInput) {
|
|
951
|
+
finalKeyInput.value = (customKeyInput.value || "").trim().toUpperCase();
|
|
952
|
+
}
|
|
953
|
+
if (customValueInput && finalValueInput) {
|
|
954
|
+
finalValueInput.value = customValueInput.value;
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
if (finalKeyInput) {
|
|
958
|
+
finalKeyInput.value = presetKeyMap[preset] || "";
|
|
959
|
+
}
|
|
960
|
+
if (finalValueInput) {
|
|
961
|
+
finalValueInput.value = buildPresetValue(preset);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (customKeyInput) {
|
|
967
|
+
customKeyInput.addEventListener("input", () => syncHiddenFields("custom"));
|
|
968
|
+
}
|
|
969
|
+
if (customValueInput) {
|
|
970
|
+
customValueInput.addEventListener("input", () => syncHiddenFields("custom"));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (secretPreset) {
|
|
974
|
+
secretPreset.addEventListener("change", updateSecretPresetForm);
|
|
975
|
+
updateSecretPresetForm();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const presetFieldIds = [];
|
|
979
|
+
for (const fields of Object.values(presetValueFields)) {
|
|
980
|
+
for (const fieldId of fields) {
|
|
981
|
+
if (!presetFieldIds.includes(fieldId)) {
|
|
982
|
+
presetFieldIds.push(fieldId);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
for (const fieldId of presetFieldIds) {
|
|
987
|
+
const el = document.getElementById(fieldId);
|
|
988
|
+
if (!el) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
el.addEventListener("input", () => syncHiddenFields(secretPreset ? secretPreset.value : ""));
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const secretsTable = document.getElementById("secrets-table");
|
|
995
|
+
const secretFormElement = document.getElementById("secret-form");
|
|
996
|
+
const secretDescriptionInput = document.querySelector("#secret-form input[name='description']");
|
|
997
|
+
const presetSelect = secretPreset;
|
|
998
|
+
const projectName = {{ selected_project|default('')|tojson }};
|
|
999
|
+
const secretCache = new Map();
|
|
1000
|
+
|
|
1001
|
+
async function fetchSecretValue(key) {
|
|
1002
|
+
if (!projectName) {
|
|
1003
|
+
throw new Error("Project not selected.");
|
|
1004
|
+
}
|
|
1005
|
+
if (secretCache.has(key)) {
|
|
1006
|
+
return secretCache.get(key);
|
|
1007
|
+
}
|
|
1008
|
+
const response = await fetch(`/projects/${encodeURIComponent(projectName)}/secrets/${encodeURIComponent(key)}/value`, { credentials: "same-origin" });
|
|
1009
|
+
const payload = await response.json();
|
|
1010
|
+
if (!response.ok || !payload.ok) {
|
|
1011
|
+
throw new Error(payload.error || "Unable to load secret value.");
|
|
1012
|
+
}
|
|
1013
|
+
secretCache.set(key, payload);
|
|
1014
|
+
return payload;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (secretsTable) {
|
|
1018
|
+
secretsTable.addEventListener("click", async (event) => {
|
|
1019
|
+
const button = event.target.closest("[data-edit-secret]");
|
|
1020
|
+
if (!button) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
event.preventDefault();
|
|
1024
|
+
const key = button.getAttribute("data-edit-secret");
|
|
1025
|
+
if (!key) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
const payload = await fetchSecretValue(key);
|
|
1030
|
+
if (customKeyInput) {
|
|
1031
|
+
customKeyInput.value = key;
|
|
1032
|
+
}
|
|
1033
|
+
if (finalKeyInput) {
|
|
1034
|
+
finalKeyInput.value = key;
|
|
1035
|
+
}
|
|
1036
|
+
if (customValueInput) {
|
|
1037
|
+
customValueInput.value = payload.value ?? "";
|
|
1038
|
+
}
|
|
1039
|
+
if (finalValueInput) {
|
|
1040
|
+
finalValueInput.value = payload.value ?? "";
|
|
1041
|
+
}
|
|
1042
|
+
if (secretDescriptionInput) {
|
|
1043
|
+
secretDescriptionInput.value = payload.description ?? "";
|
|
1044
|
+
}
|
|
1045
|
+
if (secretFormElement) {
|
|
1046
|
+
secretFormElement.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1047
|
+
}
|
|
1048
|
+
if (presetSelect) {
|
|
1049
|
+
presetSelect.value = "custom";
|
|
1050
|
+
updateSecretPresetForm();
|
|
1051
|
+
}
|
|
1052
|
+
if (customValueInput) {
|
|
1053
|
+
customValueInput.focus();
|
|
1054
|
+
}
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
window.alert(error.message);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
</script>
|
|
1061
|
+
</body>
|
|
1062
|
+
</html>
|