@yackey-labs/yauth-ui-solidjs 0.12.3 → 0.12.4
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/dist/index.js +1990 -404
- package/package.json +3 -3
- package/src/components/audit-destination-create.tsx +410 -0
- package/src/components/audit-destination-list.test.tsx +87 -0
- package/src/components/audit-destination-list.tsx +134 -0
- package/src/components/domain-claim.tsx +121 -0
- package/src/components/domain-list.tsx +94 -0
- package/src/components/domain-verify-step.tsx +81 -0
- package/src/components/invitation-accept.tsx +82 -0
- package/src/components/invite-form.tsx +108 -0
- package/src/components/member-list.tsx +214 -0
- package/src/components/organization-card.tsx +45 -0
- package/src/components/organization-create.tsx +147 -0
- package/src/components/organization-detail.tsx +68 -0
- package/src/components/organization-list.tsx +54 -0
- package/src/components/organization-switcher.tsx +139 -0
- package/src/components/role-selector.tsx +32 -0
- package/src/components/saml-connection-form.tsx +348 -0
- package/src/components/saml-login-button.tsx +53 -0
- package/src/components/scim-settings-panel.tsx +191 -0
- package/src/components/sso-connection-form.tsx +265 -0
- package/src/components/sso-connection-list.tsx +158 -0
- package/src/components/sso-login-button.tsx +46 -0
- package/src/components/transfer-ownership.tsx +122 -0
- package/src/hooks/create-active-org.ts +98 -0
- package/src/hooks/create-audit-destinations.ts +179 -0
- package/src/hooks/create-organizations.ts +495 -0
- package/src/hooks/create-sso-connections.ts +132 -0
- package/src/index.ts +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yackey-labs/yauth-ui-solidjs",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/yackey-labs/yauth"
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@simplewebauthn/browser": "^13.0.0",
|
|
25
25
|
"solid-js": "^1.9.5",
|
|
26
|
-
"@yackey-labs/yauth-client": "0.12.
|
|
27
|
-
"@yackey-labs/yauth-shared": "0.12.
|
|
26
|
+
"@yackey-labs/yauth-client": "0.12.4",
|
|
27
|
+
"@yackey-labs/yauth-shared": "0.12.4"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"happy-dom": "^20.9.0",
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { Show, createSignal } from "solid-js";
|
|
2
|
+
import {
|
|
3
|
+
type CreateDestinationInput,
|
|
4
|
+
createAuditDestinations,
|
|
5
|
+
} from "../hooks/create-audit-destinations";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Audit-export admin add-destination form (issue #96).
|
|
9
|
+
*
|
|
10
|
+
* Mirror of the Vue component — kind switcher (webhook / syslog / s3 /
|
|
11
|
+
* splunk / datadog), per-kind fields, and an HMAC secret password input
|
|
12
|
+
* for webhooks with a pointer to the docs verifier helper.
|
|
13
|
+
*
|
|
14
|
+
* Splunk + Datadog branches still warn the user that they're "follow-up";
|
|
15
|
+
* the warnings stay so the wire-shape feedback is unambiguous if the
|
|
16
|
+
* operator picks them in a version of yauth that pre-dates Phase B. The
|
|
17
|
+
* dispatcher itself does the real round-trip in Phase B.
|
|
18
|
+
*/
|
|
19
|
+
export interface AuditDestinationCreateProps {
|
|
20
|
+
organizationId?: string | null;
|
|
21
|
+
onCreated?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AuditDestinationCreate(props: AuditDestinationCreateProps) {
|
|
25
|
+
const { create, submitting, error } = createAuditDestinations(
|
|
26
|
+
() => props.organizationId ?? null,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const [kindTag, setKindTag] = createSignal<
|
|
30
|
+
"webhook" | "syslog" | "s3" | "splunk" | "datadog"
|
|
31
|
+
>("webhook");
|
|
32
|
+
const [name, setName] = createSignal("");
|
|
33
|
+
|
|
34
|
+
// Webhook
|
|
35
|
+
const [webhookUrl, setWebhookUrl] = createSignal("");
|
|
36
|
+
const [webhookFormat, setWebhookFormat] = createSignal<
|
|
37
|
+
"json" | "cef" | "rfc5424"
|
|
38
|
+
>("json");
|
|
39
|
+
const [webhookHmac, setWebhookHmac] = createSignal("");
|
|
40
|
+
|
|
41
|
+
// Syslog
|
|
42
|
+
const [syslogHost, setSyslogHost] = createSignal("");
|
|
43
|
+
const [syslogPort, setSyslogPort] = createSignal(6514);
|
|
44
|
+
const [syslogTransport, setSyslogTransport] = createSignal<
|
|
45
|
+
"tcp" | "udp" | "tls"
|
|
46
|
+
>("tcp");
|
|
47
|
+
const [syslogFacility, setSyslogFacility] = createSignal(13);
|
|
48
|
+
|
|
49
|
+
// S3
|
|
50
|
+
const [s3Bucket, setS3Bucket] = createSignal("");
|
|
51
|
+
const [s3Prefix, setS3Prefix] = createSignal("audit");
|
|
52
|
+
const [s3Region, setS3Region] = createSignal("us-east-1");
|
|
53
|
+
const [s3Partition, setS3Partition] = createSignal<
|
|
54
|
+
"by_date" | "by_org" | "by_date_and_org"
|
|
55
|
+
>("by_date");
|
|
56
|
+
|
|
57
|
+
// Splunk
|
|
58
|
+
const [splunkHecUrl, setSplunkHecUrl] = createSignal("");
|
|
59
|
+
const [splunkHecToken, setSplunkHecToken] = createSignal("");
|
|
60
|
+
|
|
61
|
+
// Datadog
|
|
62
|
+
const [ddSite, setDdSite] = createSignal("datadoghq.com");
|
|
63
|
+
const [ddApiKey, setDdApiKey] = createSignal("");
|
|
64
|
+
|
|
65
|
+
const handleSubmit = async (e: Event) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
let kind: CreateDestinationInput["kind"];
|
|
68
|
+
switch (kindTag()) {
|
|
69
|
+
case "webhook":
|
|
70
|
+
kind = {
|
|
71
|
+
type: "webhook",
|
|
72
|
+
url: webhookUrl().trim(),
|
|
73
|
+
format: webhookFormat(),
|
|
74
|
+
hmac_secret: webhookHmac().trim() || undefined,
|
|
75
|
+
};
|
|
76
|
+
break;
|
|
77
|
+
case "syslog":
|
|
78
|
+
kind = {
|
|
79
|
+
type: "syslog",
|
|
80
|
+
host: syslogHost().trim(),
|
|
81
|
+
port: syslogPort(),
|
|
82
|
+
transport: syslogTransport(),
|
|
83
|
+
facility: syslogFacility(),
|
|
84
|
+
};
|
|
85
|
+
break;
|
|
86
|
+
case "s3":
|
|
87
|
+
kind = {
|
|
88
|
+
type: "s3",
|
|
89
|
+
bucket: s3Bucket().trim(),
|
|
90
|
+
prefix: s3Prefix().trim(),
|
|
91
|
+
region: s3Region().trim(),
|
|
92
|
+
partition: s3Partition(),
|
|
93
|
+
};
|
|
94
|
+
break;
|
|
95
|
+
case "splunk":
|
|
96
|
+
kind = {
|
|
97
|
+
type: "splunk",
|
|
98
|
+
hec_url: splunkHecUrl().trim(),
|
|
99
|
+
hec_token: splunkHecToken().trim(),
|
|
100
|
+
};
|
|
101
|
+
break;
|
|
102
|
+
case "datadog":
|
|
103
|
+
kind = {
|
|
104
|
+
type: "datadog",
|
|
105
|
+
site: ddSite().trim(),
|
|
106
|
+
api_key: ddApiKey().trim(),
|
|
107
|
+
};
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const created = await create({
|
|
111
|
+
name: name().trim(),
|
|
112
|
+
organization_id: props.organizationId ?? null,
|
|
113
|
+
kind,
|
|
114
|
+
});
|
|
115
|
+
if (created) {
|
|
116
|
+
props.onCreated?.();
|
|
117
|
+
setName("");
|
|
118
|
+
setWebhookUrl("");
|
|
119
|
+
setWebhookHmac("");
|
|
120
|
+
setSyslogHost("");
|
|
121
|
+
setS3Bucket("");
|
|
122
|
+
setSplunkHecUrl("");
|
|
123
|
+
setSplunkHecToken("");
|
|
124
|
+
setDdApiKey("");
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<form
|
|
130
|
+
class="space-y-3 rounded-md border border-border p-4"
|
|
131
|
+
onSubmit={handleSubmit}
|
|
132
|
+
>
|
|
133
|
+
<h3 class="text-base font-semibold">Add destination</h3>
|
|
134
|
+
|
|
135
|
+
<Show when={error()}>
|
|
136
|
+
<div
|
|
137
|
+
class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
138
|
+
role="alert"
|
|
139
|
+
aria-live="polite"
|
|
140
|
+
>
|
|
141
|
+
{error()}
|
|
142
|
+
</div>
|
|
143
|
+
</Show>
|
|
144
|
+
|
|
145
|
+
<label class="block">
|
|
146
|
+
<span class="text-xs font-medium">Name</span>
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
required
|
|
150
|
+
value={name()}
|
|
151
|
+
onInput={(e) => setName(e.currentTarget.value)}
|
|
152
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
|
153
|
+
/>
|
|
154
|
+
</label>
|
|
155
|
+
|
|
156
|
+
<label class="block">
|
|
157
|
+
<span class="text-xs font-medium">Kind</span>
|
|
158
|
+
<select
|
|
159
|
+
value={kindTag()}
|
|
160
|
+
onChange={(e) =>
|
|
161
|
+
setKindTag(
|
|
162
|
+
e.currentTarget.value as
|
|
163
|
+
| "webhook"
|
|
164
|
+
| "syslog"
|
|
165
|
+
| "s3"
|
|
166
|
+
| "splunk"
|
|
167
|
+
| "datadog",
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
|
171
|
+
>
|
|
172
|
+
<option value="webhook">Webhook (HTTPS POST)</option>
|
|
173
|
+
<option value="syslog">Syslog (RFC 5424)</option>
|
|
174
|
+
<option value="s3">S3-compatible object storage</option>
|
|
175
|
+
<option value="splunk">Splunk HEC</option>
|
|
176
|
+
<option value="datadog">Datadog logs</option>
|
|
177
|
+
</select>
|
|
178
|
+
</label>
|
|
179
|
+
|
|
180
|
+
<Show when={kindTag() === "webhook"}>
|
|
181
|
+
<fieldset class="space-y-2">
|
|
182
|
+
<label class="block">
|
|
183
|
+
<span class="text-xs font-medium">URL</span>
|
|
184
|
+
<input
|
|
185
|
+
type="url"
|
|
186
|
+
required
|
|
187
|
+
placeholder="https://hooks.example.com/yauth-audit"
|
|
188
|
+
value={webhookUrl()}
|
|
189
|
+
onInput={(e) => setWebhookUrl(e.currentTarget.value)}
|
|
190
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
191
|
+
/>
|
|
192
|
+
</label>
|
|
193
|
+
<label class="block">
|
|
194
|
+
<span class="text-xs font-medium">Format</span>
|
|
195
|
+
<select
|
|
196
|
+
value={webhookFormat()}
|
|
197
|
+
onChange={(e) =>
|
|
198
|
+
setWebhookFormat(
|
|
199
|
+
e.currentTarget.value as "json" | "cef" | "rfc5424",
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
203
|
+
>
|
|
204
|
+
<option value="json">JSON</option>
|
|
205
|
+
<option value="cef">CEF</option>
|
|
206
|
+
<option value="rfc5424">RFC 5424 syslog body</option>
|
|
207
|
+
</select>
|
|
208
|
+
</label>
|
|
209
|
+
<label class="block">
|
|
210
|
+
<span class="text-xs font-medium">HMAC secret (optional)</span>
|
|
211
|
+
<input
|
|
212
|
+
type="password"
|
|
213
|
+
placeholder="cryptographic random, 32 bytes recommended"
|
|
214
|
+
value={webhookHmac()}
|
|
215
|
+
onInput={(e) => setWebhookHmac(e.currentTarget.value)}
|
|
216
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
217
|
+
/>
|
|
218
|
+
<p class="mt-1 text-xs text-muted-foreground">
|
|
219
|
+
When set, each request carries{" "}
|
|
220
|
+
<code class="font-mono">
|
|
221
|
+
X-Yauth-Signature: t=<unix>,v1=<hex>
|
|
222
|
+
</code>
|
|
223
|
+
. See <code>docs/audit-export/webhook.md</code> for the verifier
|
|
224
|
+
helper (Node + Python).
|
|
225
|
+
</p>
|
|
226
|
+
</label>
|
|
227
|
+
</fieldset>
|
|
228
|
+
</Show>
|
|
229
|
+
|
|
230
|
+
<Show when={kindTag() === "syslog"}>
|
|
231
|
+
<fieldset class="space-y-2">
|
|
232
|
+
<label class="block">
|
|
233
|
+
<span class="text-xs font-medium">Host</span>
|
|
234
|
+
<input
|
|
235
|
+
type="text"
|
|
236
|
+
required
|
|
237
|
+
placeholder="siem.example.internal"
|
|
238
|
+
value={syslogHost()}
|
|
239
|
+
onInput={(e) => setSyslogHost(e.currentTarget.value)}
|
|
240
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
241
|
+
/>
|
|
242
|
+
</label>
|
|
243
|
+
<label class="block">
|
|
244
|
+
<span class="text-xs font-medium">Port</span>
|
|
245
|
+
<input
|
|
246
|
+
type="number"
|
|
247
|
+
required
|
|
248
|
+
min="1"
|
|
249
|
+
max="65535"
|
|
250
|
+
value={syslogPort()}
|
|
251
|
+
onInput={(e) =>
|
|
252
|
+
setSyslogPort(Number(e.currentTarget.value) || 0)
|
|
253
|
+
}
|
|
254
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
255
|
+
/>
|
|
256
|
+
</label>
|
|
257
|
+
<label class="block">
|
|
258
|
+
<span class="text-xs font-medium">Transport</span>
|
|
259
|
+
<select
|
|
260
|
+
value={syslogTransport()}
|
|
261
|
+
onChange={(e) =>
|
|
262
|
+
setSyslogTransport(
|
|
263
|
+
e.currentTarget.value as "tcp" | "udp" | "tls",
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
267
|
+
>
|
|
268
|
+
<option value="tcp">TCP (RFC 6587 octet-counted)</option>
|
|
269
|
+
<option value="udp">
|
|
270
|
+
UDP (unauthenticated, not recommended)
|
|
271
|
+
</option>
|
|
272
|
+
<option value="tls">TCP + TLS (port 6514)</option>
|
|
273
|
+
</select>
|
|
274
|
+
</label>
|
|
275
|
+
<label class="block">
|
|
276
|
+
<span class="text-xs font-medium">Facility (0–23)</span>
|
|
277
|
+
<input
|
|
278
|
+
type="number"
|
|
279
|
+
required
|
|
280
|
+
min="0"
|
|
281
|
+
max="23"
|
|
282
|
+
value={syslogFacility()}
|
|
283
|
+
onInput={(e) =>
|
|
284
|
+
setSyslogFacility(Number(e.currentTarget.value) || 0)
|
|
285
|
+
}
|
|
286
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
287
|
+
/>
|
|
288
|
+
</label>
|
|
289
|
+
</fieldset>
|
|
290
|
+
</Show>
|
|
291
|
+
|
|
292
|
+
<Show when={kindTag() === "s3"}>
|
|
293
|
+
<fieldset class="space-y-2">
|
|
294
|
+
<label class="block">
|
|
295
|
+
<span class="text-xs font-medium">Bucket</span>
|
|
296
|
+
<input
|
|
297
|
+
type="text"
|
|
298
|
+
required
|
|
299
|
+
value={s3Bucket()}
|
|
300
|
+
onInput={(e) => setS3Bucket(e.currentTarget.value)}
|
|
301
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
302
|
+
/>
|
|
303
|
+
</label>
|
|
304
|
+
<label class="block">
|
|
305
|
+
<span class="text-xs font-medium">Prefix</span>
|
|
306
|
+
<input
|
|
307
|
+
type="text"
|
|
308
|
+
value={s3Prefix()}
|
|
309
|
+
onInput={(e) => setS3Prefix(e.currentTarget.value)}
|
|
310
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
311
|
+
/>
|
|
312
|
+
</label>
|
|
313
|
+
<label class="block">
|
|
314
|
+
<span class="text-xs font-medium">Region</span>
|
|
315
|
+
<input
|
|
316
|
+
type="text"
|
|
317
|
+
required
|
|
318
|
+
value={s3Region()}
|
|
319
|
+
onInput={(e) => setS3Region(e.currentTarget.value)}
|
|
320
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
321
|
+
/>
|
|
322
|
+
</label>
|
|
323
|
+
<label class="block">
|
|
324
|
+
<span class="text-xs font-medium">Partition</span>
|
|
325
|
+
<select
|
|
326
|
+
value={s3Partition()}
|
|
327
|
+
onChange={(e) =>
|
|
328
|
+
setS3Partition(
|
|
329
|
+
e.currentTarget.value as
|
|
330
|
+
| "by_date"
|
|
331
|
+
| "by_org"
|
|
332
|
+
| "by_date_and_org",
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
336
|
+
>
|
|
337
|
+
<option value="by_date">By date</option>
|
|
338
|
+
<option value="by_org">By org</option>
|
|
339
|
+
<option value="by_date_and_org">By date and org</option>
|
|
340
|
+
</select>
|
|
341
|
+
</label>
|
|
342
|
+
</fieldset>
|
|
343
|
+
</Show>
|
|
344
|
+
|
|
345
|
+
<Show when={kindTag() === "splunk"}>
|
|
346
|
+
<fieldset class="space-y-2">
|
|
347
|
+
<label class="block">
|
|
348
|
+
<span class="text-xs font-medium">HEC URL</span>
|
|
349
|
+
<input
|
|
350
|
+
type="url"
|
|
351
|
+
required
|
|
352
|
+
placeholder="https://splunk.example.com:8088"
|
|
353
|
+
value={splunkHecUrl()}
|
|
354
|
+
onInput={(e) => setSplunkHecUrl(e.currentTarget.value)}
|
|
355
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
356
|
+
/>
|
|
357
|
+
</label>
|
|
358
|
+
<label class="block">
|
|
359
|
+
<span class="text-xs font-medium">HEC token</span>
|
|
360
|
+
<input
|
|
361
|
+
type="password"
|
|
362
|
+
required
|
|
363
|
+
value={splunkHecToken()}
|
|
364
|
+
onInput={(e) => setSplunkHecToken(e.currentTarget.value)}
|
|
365
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
366
|
+
/>
|
|
367
|
+
</label>
|
|
368
|
+
</fieldset>
|
|
369
|
+
</Show>
|
|
370
|
+
|
|
371
|
+
<Show when={kindTag() === "datadog"}>
|
|
372
|
+
<fieldset class="space-y-2">
|
|
373
|
+
<label class="block">
|
|
374
|
+
<span class="text-xs font-medium">Site</span>
|
|
375
|
+
<select
|
|
376
|
+
value={ddSite()}
|
|
377
|
+
onChange={(e) => setDdSite(e.currentTarget.value)}
|
|
378
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
379
|
+
>
|
|
380
|
+
<option value="datadoghq.com">datadoghq.com (US1)</option>
|
|
381
|
+
<option value="us3.datadoghq.com">us3.datadoghq.com (US3)</option>
|
|
382
|
+
<option value="us5.datadoghq.com">us5.datadoghq.com (US5)</option>
|
|
383
|
+
<option value="datadoghq.eu">datadoghq.eu (EU)</option>
|
|
384
|
+
<option value="ap1.datadoghq.com">ap1.datadoghq.com (AP1)</option>
|
|
385
|
+
<option value="ddog-gov.com">ddog-gov.com (US1-FED)</option>
|
|
386
|
+
</select>
|
|
387
|
+
</label>
|
|
388
|
+
<label class="block">
|
|
389
|
+
<span class="text-xs font-medium">API key</span>
|
|
390
|
+
<input
|
|
391
|
+
type="password"
|
|
392
|
+
required
|
|
393
|
+
value={ddApiKey()}
|
|
394
|
+
onInput={(e) => setDdApiKey(e.currentTarget.value)}
|
|
395
|
+
class="mt-1 block w-full rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm"
|
|
396
|
+
/>
|
|
397
|
+
</label>
|
|
398
|
+
</fieldset>
|
|
399
|
+
</Show>
|
|
400
|
+
|
|
401
|
+
<button
|
|
402
|
+
type="submit"
|
|
403
|
+
disabled={submitting()}
|
|
404
|
+
class="inline-flex h-9 cursor-pointer items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
|
|
405
|
+
>
|
|
406
|
+
Add destination
|
|
407
|
+
</button>
|
|
408
|
+
</form>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for the SolidJS audit-destination hook (issue #96).
|
|
3
|
+
*
|
|
4
|
+
* We test the headless `createAuditDestinations` hook (signal-only, no
|
|
5
|
+
* DOM) so the test suite doesn't need a Solid DOM testing-library. The
|
|
6
|
+
* component itself is a thin wrapper around this hook — covered by
|
|
7
|
+
* typecheck + build.
|
|
8
|
+
*/
|
|
9
|
+
import { configureClient } from "@yackey-labs/yauth-client";
|
|
10
|
+
import { createRoot } from "solid-js";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { createAuditDestinations } from "../hooks/create-audit-destinations";
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// The typed client reads its baseUrl from the global config; tests
|
|
16
|
+
// install an empty baseUrl so the mocked `fetch` sees the raw paths.
|
|
17
|
+
configureClient({ baseUrl: "" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function mockFetch(response: unknown, status = 200) {
|
|
21
|
+
const text = typeof response === "string" ? response : JSON.stringify(response);
|
|
22
|
+
const fn = vi.fn().mockResolvedValue({
|
|
23
|
+
ok: status >= 200 && status < 300,
|
|
24
|
+
status,
|
|
25
|
+
statusText: status === 200 ? "OK" : "ERR",
|
|
26
|
+
text: () => Promise.resolve(text),
|
|
27
|
+
json: () => Promise.resolve(response),
|
|
28
|
+
});
|
|
29
|
+
(globalThis as unknown as { fetch: typeof fetch }).fetch =
|
|
30
|
+
fn as unknown as typeof fetch;
|
|
31
|
+
return fn;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("createAuditDestinations", () => {
|
|
39
|
+
it("fetches destinations on mount and exposes them via the signal", async () => {
|
|
40
|
+
mockFetch([
|
|
41
|
+
{
|
|
42
|
+
id: "019e36bf-1234-7000-8000-aaaaaaaaaaaa",
|
|
43
|
+
organization_id: null,
|
|
44
|
+
name: "prod-splunk",
|
|
45
|
+
kind_tag: "splunk",
|
|
46
|
+
kind: { type: "splunk", hec_url: "https://splunk.example.com" },
|
|
47
|
+
status: "active",
|
|
48
|
+
last_success_at: null,
|
|
49
|
+
last_failure_at: null,
|
|
50
|
+
created_at: "2026-05-17T12:00:00Z",
|
|
51
|
+
updated_at: "2026-05-17T12:00:00Z",
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
await createRoot(async (dispose) => {
|
|
56
|
+
const hook = createAuditDestinations(() => null);
|
|
57
|
+
// Allow the createEffect → refresh chain to run.
|
|
58
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
59
|
+
expect(hook.destinations()).toHaveLength(1);
|
|
60
|
+
expect(hook.destinations()[0]?.name).toBe("prod-splunk");
|
|
61
|
+
expect(hook.error()).toBeNull();
|
|
62
|
+
dispose();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("surfaces fetch errors via the error signal", async () => {
|
|
67
|
+
mockFetch({ error: "audit-export down" }, 500);
|
|
68
|
+
await createRoot(async (dispose) => {
|
|
69
|
+
const hook = createAuditDestinations(() => null);
|
|
70
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
71
|
+
expect(hook.destinations()).toHaveLength(0);
|
|
72
|
+
expect(hook.error()).toMatch(/audit-export down/);
|
|
73
|
+
dispose();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("includes scope query string based on the org accessor", async () => {
|
|
78
|
+
const fetchSpy = mockFetch([]);
|
|
79
|
+
await createRoot(async (dispose) => {
|
|
80
|
+
createAuditDestinations(() => "org-uuid-123");
|
|
81
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
82
|
+
const callUrl = (fetchSpy.mock.calls[0]?.[0] as string) ?? "";
|
|
83
|
+
expect(callUrl).toContain("organization_id=org-uuid-123");
|
|
84
|
+
dispose();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { For, Show, createMemo } from "solid-js";
|
|
2
|
+
import { createAuditDestinations } from "../hooks/create-audit-destinations";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Audit-export admin destinations list (issue #96).
|
|
6
|
+
*
|
|
7
|
+
* Mirror of the Vue component — shows deployment-wide OR per-org
|
|
8
|
+
* destinations with their sanitized kind JSON, status badge, and
|
|
9
|
+
* last_success_at / last_failure_at timestamps. No secrets are ever
|
|
10
|
+
* displayed (the server strips `hmac_secret`, `hec_token`, Datadog
|
|
11
|
+
* `api_key` before returning).
|
|
12
|
+
*/
|
|
13
|
+
export interface AuditDestinationListProps {
|
|
14
|
+
/**
|
|
15
|
+
* `null` => deployment-wide destinations. A UUID string => per-org
|
|
16
|
+
* destinations for that org.
|
|
17
|
+
*/
|
|
18
|
+
organizationId?: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fmtDate(s: string | null | undefined): string {
|
|
22
|
+
if (!s) return "—";
|
|
23
|
+
try {
|
|
24
|
+
return new Date(s).toLocaleString();
|
|
25
|
+
} catch {
|
|
26
|
+
return s;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function AuditDestinationList(props: AuditDestinationListProps) {
|
|
31
|
+
const scope = createMemo(() => props.organizationId ?? null);
|
|
32
|
+
const { destinations, loading, error, disable, remove, submitting, refresh } =
|
|
33
|
+
createAuditDestinations(() => scope());
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<section class="space-y-4">
|
|
37
|
+
<header class="flex items-center justify-between">
|
|
38
|
+
<h2 class="text-lg font-semibold">
|
|
39
|
+
{props.organizationId
|
|
40
|
+
? "Per-org SIEM destinations"
|
|
41
|
+
: "Deployment-wide SIEM destinations"}
|
|
42
|
+
</h2>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-transparent px-3 text-xs font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
46
|
+
disabled={loading() || submitting()}
|
|
47
|
+
onClick={() => void refresh()}
|
|
48
|
+
>
|
|
49
|
+
Refresh
|
|
50
|
+
</button>
|
|
51
|
+
</header>
|
|
52
|
+
|
|
53
|
+
<Show when={error()}>
|
|
54
|
+
<div
|
|
55
|
+
class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
56
|
+
role="alert"
|
|
57
|
+
aria-live="polite"
|
|
58
|
+
>
|
|
59
|
+
{error()}
|
|
60
|
+
</div>
|
|
61
|
+
</Show>
|
|
62
|
+
|
|
63
|
+
<Show when={loading()}>
|
|
64
|
+
<p class="text-sm text-muted-foreground">Loading…</p>
|
|
65
|
+
</Show>
|
|
66
|
+
|
|
67
|
+
<Show
|
|
68
|
+
when={!loading() && destinations().length > 0}
|
|
69
|
+
fallback={
|
|
70
|
+
<Show when={!loading()}>
|
|
71
|
+
<p class="text-sm text-muted-foreground">
|
|
72
|
+
No destinations configured. Use the “Add destination” form below.
|
|
73
|
+
</p>
|
|
74
|
+
</Show>
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
<ul class="divide-y divide-border">
|
|
78
|
+
<For each={destinations()}>
|
|
79
|
+
{(d) => (
|
|
80
|
+
<li class="flex items-start justify-between gap-3 py-3">
|
|
81
|
+
<div class="min-w-0 flex-1 space-y-1">
|
|
82
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
83
|
+
<span class="font-medium">{d.name}</span>
|
|
84
|
+
<span class="rounded-full bg-muted px-2 py-0.5 text-xs uppercase text-muted-foreground">
|
|
85
|
+
{d.kind_tag}
|
|
86
|
+
</span>
|
|
87
|
+
<span
|
|
88
|
+
classList={{
|
|
89
|
+
"rounded-full px-2 py-0.5 text-xs": true,
|
|
90
|
+
"bg-emerald-500/10 text-emerald-700":
|
|
91
|
+
d.status === "active",
|
|
92
|
+
"bg-muted text-muted-foreground":
|
|
93
|
+
d.status === "disabled",
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{d.status}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
<pre class="overflow-x-auto rounded-md bg-muted/50 p-2 text-xs leading-snug">
|
|
100
|
+
{JSON.stringify(d.kind, null, 2)}
|
|
101
|
+
</pre>
|
|
102
|
+
<p class="text-xs text-muted-foreground">
|
|
103
|
+
Last success: {fmtDate(d.last_success_at)} · Last failure:{" "}
|
|
104
|
+
{fmtDate(d.last_failure_at)}
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="flex flex-col gap-2">
|
|
108
|
+
<Show when={d.status === "active"}>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-transparent px-3 text-xs font-medium shadow-sm hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
112
|
+
disabled={submitting()}
|
|
113
|
+
onClick={() => void disable(d.id)}
|
|
114
|
+
>
|
|
115
|
+
Disable
|
|
116
|
+
</button>
|
|
117
|
+
</Show>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-destructive bg-transparent px-3 text-xs font-medium text-destructive shadow-sm hover:bg-destructive/10 disabled:pointer-events-none disabled:opacity-50"
|
|
121
|
+
disabled={submitting()}
|
|
122
|
+
onClick={() => void remove(d.id)}
|
|
123
|
+
>
|
|
124
|
+
Delete
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</li>
|
|
128
|
+
)}
|
|
129
|
+
</For>
|
|
130
|
+
</ul>
|
|
131
|
+
</Show>
|
|
132
|
+
</section>
|
|
133
|
+
);
|
|
134
|
+
}
|