@yackey-labs/yauth-ui-solidjs 0.12.2 → 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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Hook for the audit-export admin surface (issue #96).
|
|
2
|
+
//
|
|
3
|
+
// Phase B: now uses the typed `@yackey-labs/yauth-client` after the
|
|
4
|
+
// routes_meta.rs regeneration registered `/audit/destinations` and friends.
|
|
5
|
+
// The raw-fetch fallback from the initial drop is gone — every call goes
|
|
6
|
+
// through the generated client functions, which route via the shared
|
|
7
|
+
// `customFetch` mutator.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AuditDestinationResponse,
|
|
11
|
+
CreateAuditDestinationRequest,
|
|
12
|
+
UpdateAuditDestinationRequest,
|
|
13
|
+
} from "@yackey-labs/yauth-client";
|
|
14
|
+
import {
|
|
15
|
+
auditExportCreateDestination,
|
|
16
|
+
auditExportDeleteDestination,
|
|
17
|
+
auditExportListDestinationOutbox,
|
|
18
|
+
auditExportListDestinations,
|
|
19
|
+
auditExportUpdateDestination,
|
|
20
|
+
} from "@yackey-labs/yauth-client";
|
|
21
|
+
import { type Accessor, createEffect, createSignal } from "solid-js";
|
|
22
|
+
|
|
23
|
+
export type AuditDestinationKindTag =
|
|
24
|
+
| "webhook"
|
|
25
|
+
| "syslog"
|
|
26
|
+
| "s3"
|
|
27
|
+
| "splunk"
|
|
28
|
+
| "datadog";
|
|
29
|
+
|
|
30
|
+
export type AuditDestination = AuditDestinationResponse;
|
|
31
|
+
|
|
32
|
+
export interface OutboxEntry {
|
|
33
|
+
id: string;
|
|
34
|
+
audit_log_id: string;
|
|
35
|
+
destination_id: string;
|
|
36
|
+
status: "pending" | "sent" | "failed" | "dead_letter";
|
|
37
|
+
attempts: number;
|
|
38
|
+
last_attempt_at: string | null;
|
|
39
|
+
last_error: string | null;
|
|
40
|
+
created_at: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CreateDestinationInput = Omit<
|
|
44
|
+
CreateAuditDestinationRequest,
|
|
45
|
+
"kind"
|
|
46
|
+
> & {
|
|
47
|
+
kind:
|
|
48
|
+
| {
|
|
49
|
+
type: "webhook";
|
|
50
|
+
url: string;
|
|
51
|
+
format?: "json" | "cef" | "rfc5424";
|
|
52
|
+
hmac_secret?: string;
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
type: "syslog";
|
|
57
|
+
host: string;
|
|
58
|
+
port: number;
|
|
59
|
+
transport?: "tcp" | "udp" | "tls";
|
|
60
|
+
facility?: number;
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
type: "s3";
|
|
64
|
+
bucket: string;
|
|
65
|
+
prefix?: string;
|
|
66
|
+
region: string;
|
|
67
|
+
partition?: "by_date" | "by_org" | "by_date_and_org";
|
|
68
|
+
}
|
|
69
|
+
| { type: "splunk"; hec_url: string; hec_token: string }
|
|
70
|
+
| { type: "datadog"; site: string; api_key: string };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `baseUrl` is no longer required — the typed client reads its base URL
|
|
75
|
+
* from the global `configureClient` call made by the YAuthProvider. The
|
|
76
|
+
* option is kept in the signature so existing call sites compile without
|
|
77
|
+
* change.
|
|
78
|
+
*/
|
|
79
|
+
export interface CreateAuditDestinationsOptions {
|
|
80
|
+
baseUrl?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Headless hook for audit-export destination management (issue #96).
|
|
85
|
+
*
|
|
86
|
+
* Mirrors `useAuditDestinations` from the Vue package. Pass an `orgScope`
|
|
87
|
+
* accessor; the hook auto-fetches on mount and whenever the scope changes
|
|
88
|
+
* (null = deployment-wide).
|
|
89
|
+
*/
|
|
90
|
+
export function createAuditDestinations(
|
|
91
|
+
orgScope?: Accessor<string | null | undefined>,
|
|
92
|
+
_options: CreateAuditDestinationsOptions = {},
|
|
93
|
+
) {
|
|
94
|
+
const [destinations, setDestinations] = createSignal<AuditDestination[]>([]);
|
|
95
|
+
const [loading, setLoading] = createSignal(false);
|
|
96
|
+
const [submitting, setSubmitting] = createSignal(false);
|
|
97
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
98
|
+
|
|
99
|
+
const refresh = async () => {
|
|
100
|
+
setLoading(true);
|
|
101
|
+
setError(null);
|
|
102
|
+
try {
|
|
103
|
+
const org = orgScope?.();
|
|
104
|
+
const params = org
|
|
105
|
+
? { organization_id: org }
|
|
106
|
+
: { scope: "deployment" };
|
|
107
|
+
const rows = await auditExportListDestinations(params);
|
|
108
|
+
setDestinations(rows);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
setError(e instanceof Error ? e.message : "failed to load destinations");
|
|
111
|
+
setDestinations([]);
|
|
112
|
+
} finally {
|
|
113
|
+
setLoading(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const create = async (
|
|
118
|
+
input: CreateDestinationInput,
|
|
119
|
+
): Promise<AuditDestination | null> => {
|
|
120
|
+
setSubmitting(true);
|
|
121
|
+
setError(null);
|
|
122
|
+
try {
|
|
123
|
+
const created = await auditExportCreateDestination(
|
|
124
|
+
input as unknown as CreateAuditDestinationRequest,
|
|
125
|
+
);
|
|
126
|
+
await refresh();
|
|
127
|
+
return created;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
setError(e instanceof Error ? e.message : "create failed");
|
|
130
|
+
return null;
|
|
131
|
+
} finally {
|
|
132
|
+
setSubmitting(false);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const disable = async (id: string): Promise<void> => {
|
|
137
|
+
setSubmitting(true);
|
|
138
|
+
try {
|
|
139
|
+
await auditExportUpdateDestination(id, {
|
|
140
|
+
status: "disabled",
|
|
141
|
+
} as UpdateAuditDestinationRequest);
|
|
142
|
+
await refresh();
|
|
143
|
+
} finally {
|
|
144
|
+
setSubmitting(false);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const remove = async (id: string): Promise<void> => {
|
|
149
|
+
setSubmitting(true);
|
|
150
|
+
try {
|
|
151
|
+
await auditExportDeleteDestination(id);
|
|
152
|
+
await refresh();
|
|
153
|
+
} finally {
|
|
154
|
+
setSubmitting(false);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const outbox = async (id: string, limit = 50): Promise<OutboxEntry[]> =>
|
|
159
|
+
(await auditExportListDestinationOutbox(id, {
|
|
160
|
+
limit: String(limit),
|
|
161
|
+
})) as unknown as OutboxEntry[];
|
|
162
|
+
|
|
163
|
+
createEffect(() => {
|
|
164
|
+
void orgScope?.();
|
|
165
|
+
void refresh();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
destinations,
|
|
170
|
+
loading,
|
|
171
|
+
submitting,
|
|
172
|
+
error,
|
|
173
|
+
refresh,
|
|
174
|
+
create,
|
|
175
|
+
disable,
|
|
176
|
+
remove,
|
|
177
|
+
outbox,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChangeRoleRequest,
|
|
3
|
+
CreateDomainRequest,
|
|
4
|
+
CreateDomainResponse,
|
|
5
|
+
CreateInvitationRequest,
|
|
6
|
+
CreateInvitationResponse,
|
|
7
|
+
CreateOrgRequest,
|
|
8
|
+
DomainResponse,
|
|
9
|
+
MembershipResponse,
|
|
10
|
+
OrganizationResponse,
|
|
11
|
+
PermissionsResponse,
|
|
12
|
+
TransferOwnershipRequest,
|
|
13
|
+
UpdateDomainRequest,
|
|
14
|
+
UpdateOrgRequest,
|
|
15
|
+
VerifyDomainResponse,
|
|
16
|
+
} from "@yackey-labs/yauth-client";
|
|
17
|
+
import { type Accessor, createEffect, createSignal } from "solid-js";
|
|
18
|
+
import { useYAuth } from "../provider";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a URL-safe slug from a free-form name.
|
|
22
|
+
* Deterministic + idempotent — safe to recompute on every keystroke.
|
|
23
|
+
*/
|
|
24
|
+
export function slugify(input: string): string {
|
|
25
|
+
return input
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
29
|
+
.replace(/\s+/g, "-")
|
|
30
|
+
.replace(/-+/g, "-")
|
|
31
|
+
.replace(/^-|-$/g, "")
|
|
32
|
+
.slice(0, 64);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Headless hook for the current user's organization list.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: We use `createSignal` + manual fetch (rather than `createResource`)
|
|
39
|
+
* because `createResource` requires a tracked source, and we want fully
|
|
40
|
+
* imperative `refetch` semantics matching the Vue composable's shape. The
|
|
41
|
+
* active-organization concept lives in issue #89.
|
|
42
|
+
*/
|
|
43
|
+
export function createOrganizations() {
|
|
44
|
+
const { client } = useYAuth();
|
|
45
|
+
const [organizations, setOrganizations] = createSignal<OrganizationResponse[]>([]);
|
|
46
|
+
const [loading, setLoading] = createSignal(false);
|
|
47
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
48
|
+
|
|
49
|
+
const refetch = async () => {
|
|
50
|
+
if (!client?.organizations) {
|
|
51
|
+
setError("Organizations feature is not enabled on this server.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
try {
|
|
57
|
+
setOrganizations(await client.organizations.list());
|
|
58
|
+
} catch (err) {
|
|
59
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const create = async (body: CreateOrgRequest): Promise<OrganizationResponse | null> => {
|
|
66
|
+
if (!client?.organizations) {
|
|
67
|
+
setError("Organizations feature is not enabled on this server.");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
setError(null);
|
|
71
|
+
try {
|
|
72
|
+
const org = await client.organizations.create(body);
|
|
73
|
+
setOrganizations([...organizations(), org]);
|
|
74
|
+
return org;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const remove = async (id: string): Promise<boolean> => {
|
|
82
|
+
if (!client?.organizations) {
|
|
83
|
+
setError("Organizations feature is not enabled on this server.");
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
setError(null);
|
|
87
|
+
try {
|
|
88
|
+
await client.organizations.delete(id);
|
|
89
|
+
setOrganizations(organizations().filter((o) => o.id !== id));
|
|
90
|
+
return true;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
void refetch();
|
|
98
|
+
|
|
99
|
+
return { organizations, loading, error, refetch, create, remove };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Headless hook for a single organization by id accessor.
|
|
104
|
+
*/
|
|
105
|
+
export function createOrganization(id: Accessor<string | null | undefined>) {
|
|
106
|
+
const { client } = useYAuth();
|
|
107
|
+
const [organization, setOrganization] = createSignal<OrganizationResponse | null>(null);
|
|
108
|
+
const [loading, setLoading] = createSignal(false);
|
|
109
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
110
|
+
|
|
111
|
+
const refetch = async () => {
|
|
112
|
+
const current = id();
|
|
113
|
+
if (!current) {
|
|
114
|
+
setOrganization(null);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!client?.organizations) {
|
|
118
|
+
setError("Organizations feature is not enabled on this server.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
setLoading(true);
|
|
122
|
+
setError(null);
|
|
123
|
+
try {
|
|
124
|
+
setOrganization(await client.organizations.get(current));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
127
|
+
setOrganization(null);
|
|
128
|
+
} finally {
|
|
129
|
+
setLoading(false);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const update = async (body: UpdateOrgRequest): Promise<OrganizationResponse | null> => {
|
|
134
|
+
const current = id();
|
|
135
|
+
if (!current || !client?.organizations) return null;
|
|
136
|
+
setError(null);
|
|
137
|
+
try {
|
|
138
|
+
const updated = await client.organizations.update(current, body);
|
|
139
|
+
setOrganization(updated);
|
|
140
|
+
return updated;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
void refetch();
|
|
148
|
+
|
|
149
|
+
return { organization, loading, error, refetch, update };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Headless hook for an organization's members + invitation creation.
|
|
154
|
+
*/
|
|
155
|
+
export function createMembers(orgId: Accessor<string | null | undefined>) {
|
|
156
|
+
const { client } = useYAuth();
|
|
157
|
+
const [members, setMembers] = createSignal<MembershipResponse[]>([]);
|
|
158
|
+
const [loading, setLoading] = createSignal(false);
|
|
159
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
160
|
+
|
|
161
|
+
const refetch = async () => {
|
|
162
|
+
const current = orgId();
|
|
163
|
+
if (!current) {
|
|
164
|
+
setMembers([]);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!client?.organizations) {
|
|
168
|
+
setError("Organizations feature is not enabled on this server.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
setLoading(true);
|
|
172
|
+
setError(null);
|
|
173
|
+
try {
|
|
174
|
+
setMembers(await client.organizations.listMembers(current));
|
|
175
|
+
} catch (err) {
|
|
176
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
177
|
+
} finally {
|
|
178
|
+
setLoading(false);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const invite = async (
|
|
183
|
+
body: CreateInvitationRequest,
|
|
184
|
+
): Promise<CreateInvitationResponse | null> => {
|
|
185
|
+
const current = orgId();
|
|
186
|
+
if (!current || !client?.organizations) return null;
|
|
187
|
+
setError(null);
|
|
188
|
+
try {
|
|
189
|
+
return await client.organizations.createInvitation(current, body);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
void refetch();
|
|
197
|
+
|
|
198
|
+
return { members, loading, error, refetch, invite };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Headless hook for accepting an invitation by token.
|
|
203
|
+
* Stateless — caller invokes `accept` explicitly.
|
|
204
|
+
*/
|
|
205
|
+
export function createInvitation() {
|
|
206
|
+
const { client } = useYAuth();
|
|
207
|
+
const [submitting, setSubmitting] = createSignal(false);
|
|
208
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
209
|
+
|
|
210
|
+
const accept = async (token: string): Promise<MembershipResponse | null> => {
|
|
211
|
+
if (!client?.organizations) {
|
|
212
|
+
setError("Organizations feature is not enabled on this server.");
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (!token.trim()) {
|
|
216
|
+
setError("Invitation token is required.");
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
setSubmitting(true);
|
|
220
|
+
setError(null);
|
|
221
|
+
try {
|
|
222
|
+
return await client.organizations.acceptInvitation({ token });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
225
|
+
return null;
|
|
226
|
+
} finally {
|
|
227
|
+
setSubmitting(false);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return { submitting, error, accept };
|
|
232
|
+
}
|
|
233
|
+
// ──────────────────────────────────────────────
|
|
234
|
+
// RBAC (#88) hooks
|
|
235
|
+
// ──────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Built-in role string constants — mirrors `yauth::auth::rbac::roles`.
|
|
239
|
+
* Custom role strings are permitted but receive no default permissions.
|
|
240
|
+
*/
|
|
241
|
+
export const ROLES = {
|
|
242
|
+
OWNER: "owner",
|
|
243
|
+
ADMIN: "admin",
|
|
244
|
+
BILLING_ADMIN: "billing_admin",
|
|
245
|
+
MEMBER: "member",
|
|
246
|
+
VIEWER: "viewer",
|
|
247
|
+
} as const;
|
|
248
|
+
|
|
249
|
+
export type BuiltinRole = (typeof ROLES)[keyof typeof ROLES];
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Headless hook for the calling user's effective permissions in an org.
|
|
253
|
+
* Auto-fetches when `orgId` changes.
|
|
254
|
+
*/
|
|
255
|
+
export function createOrgPermissions(orgId: Accessor<string | null | undefined>) {
|
|
256
|
+
const { client } = useYAuth();
|
|
257
|
+
const [permissions, setPermissions] = createSignal<PermissionsResponse | null>(null);
|
|
258
|
+
const [loading, setLoading] = createSignal(false);
|
|
259
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
260
|
+
|
|
261
|
+
const refetch = async () => {
|
|
262
|
+
const current = orgId();
|
|
263
|
+
if (!current) {
|
|
264
|
+
setPermissions(null);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!client?.organizations) {
|
|
268
|
+
setError("Organizations feature is not enabled on this server.");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
setLoading(true);
|
|
272
|
+
setError(null);
|
|
273
|
+
try {
|
|
274
|
+
const next = await client.organizations.listPermissions(current);
|
|
275
|
+
setPermissions(next);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
278
|
+
} finally {
|
|
279
|
+
setLoading(false);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const hasPermission = (perm: string): boolean =>
|
|
284
|
+
permissions()?.permissions.includes(perm) ?? false;
|
|
285
|
+
|
|
286
|
+
const isRole = (role: string): boolean => permissions()?.role === role;
|
|
287
|
+
|
|
288
|
+
createEffect(() => {
|
|
289
|
+
orgId();
|
|
290
|
+
void refetch();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
permissions,
|
|
295
|
+
loading,
|
|
296
|
+
error,
|
|
297
|
+
refetch,
|
|
298
|
+
hasPermission,
|
|
299
|
+
isRole,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Headless hook for the change-role + remove-member + transfer actions.
|
|
305
|
+
*/
|
|
306
|
+
export function createOrgRoles(orgId: Accessor<string | null | undefined>) {
|
|
307
|
+
const { client } = useYAuth();
|
|
308
|
+
const [submitting, setSubmitting] = createSignal(false);
|
|
309
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
310
|
+
|
|
311
|
+
const changeRole = async (
|
|
312
|
+
userId: string,
|
|
313
|
+
body: ChangeRoleRequest,
|
|
314
|
+
): Promise<MembershipResponse | null> => {
|
|
315
|
+
const current = orgId();
|
|
316
|
+
if (!current || !client?.organizations) return null;
|
|
317
|
+
setSubmitting(true);
|
|
318
|
+
setError(null);
|
|
319
|
+
try {
|
|
320
|
+
return await client.organizations.changeRole(current, userId, body);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
323
|
+
return null;
|
|
324
|
+
} finally {
|
|
325
|
+
setSubmitting(false);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const removeMember = async (userId: string): Promise<boolean> => {
|
|
330
|
+
const current = orgId();
|
|
331
|
+
if (!current || !client?.organizations) return false;
|
|
332
|
+
setSubmitting(true);
|
|
333
|
+
setError(null);
|
|
334
|
+
try {
|
|
335
|
+
await client.organizations.removeMember(current, userId);
|
|
336
|
+
return true;
|
|
337
|
+
} catch (err) {
|
|
338
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
339
|
+
return false;
|
|
340
|
+
} finally {
|
|
341
|
+
setSubmitting(false);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const transferOwnership = async (body: TransferOwnershipRequest): Promise<boolean> => {
|
|
346
|
+
const current = orgId();
|
|
347
|
+
if (!current || !client?.organizations) return false;
|
|
348
|
+
setSubmitting(true);
|
|
349
|
+
setError(null);
|
|
350
|
+
try {
|
|
351
|
+
await client.organizations.transferOwnership(current, body);
|
|
352
|
+
return true;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
355
|
+
return false;
|
|
356
|
+
} finally {
|
|
357
|
+
setSubmitting(false);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return { submitting, error, changeRole, removeMember, transferOwnership };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ──────────────────────────────────────────────
|
|
365
|
+
// Verified email domains (#90)
|
|
366
|
+
// ──────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Headless hook for an organization's domain claims.
|
|
370
|
+
*
|
|
371
|
+
* Tracks the list reactively (call `refetch` after mutations). Exposes
|
|
372
|
+
* `claim`, `verify`, `update`, and `remove` operations. All mutating
|
|
373
|
+
* methods return `null` on error and surface the error via `error()`.
|
|
374
|
+
*
|
|
375
|
+
* `lastCreated` holds the most recent `CreateDomainResponse` — the
|
|
376
|
+
* `verification_token` field on that response is shown ONCE, so the UI
|
|
377
|
+
* must capture it from this signal rather than re-fetch.
|
|
378
|
+
*/
|
|
379
|
+
export function createDomains(orgId: Accessor<string | null>) {
|
|
380
|
+
const { client } = useYAuth();
|
|
381
|
+
const [domains, setDomains] = createSignal<DomainResponse[]>([]);
|
|
382
|
+
const [loading, setLoading] = createSignal(false);
|
|
383
|
+
const [submitting, setSubmitting] = createSignal(false);
|
|
384
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
385
|
+
const [lastCreated, setLastCreated] = createSignal<CreateDomainResponse | null>(null);
|
|
386
|
+
|
|
387
|
+
const refetch = async () => {
|
|
388
|
+
const current = orgId();
|
|
389
|
+
if (!current) {
|
|
390
|
+
setDomains([]);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!client?.organizations) {
|
|
394
|
+
setError("Organizations feature is not enabled on this server.");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
setLoading(true);
|
|
398
|
+
setError(null);
|
|
399
|
+
try {
|
|
400
|
+
setDomains(await client.organizations.listDomains(current));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
403
|
+
} finally {
|
|
404
|
+
setLoading(false);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const claim = async (body: CreateDomainRequest): Promise<CreateDomainResponse | null> => {
|
|
409
|
+
const current = orgId();
|
|
410
|
+
if (!current || !client?.organizations) return null;
|
|
411
|
+
setSubmitting(true);
|
|
412
|
+
setError(null);
|
|
413
|
+
try {
|
|
414
|
+
const result = await client.organizations.createDomain(current, body);
|
|
415
|
+
setLastCreated(result);
|
|
416
|
+
await refetch();
|
|
417
|
+
return result;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
420
|
+
return null;
|
|
421
|
+
} finally {
|
|
422
|
+
setSubmitting(false);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const verify = async (did: string): Promise<VerifyDomainResponse | null> => {
|
|
427
|
+
const current = orgId();
|
|
428
|
+
if (!current || !client?.organizations) return null;
|
|
429
|
+
setSubmitting(true);
|
|
430
|
+
setError(null);
|
|
431
|
+
try {
|
|
432
|
+
const result = await client.organizations.verifyDomain(current, did);
|
|
433
|
+
await refetch();
|
|
434
|
+
return result;
|
|
435
|
+
} catch (err) {
|
|
436
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
437
|
+
return null;
|
|
438
|
+
} finally {
|
|
439
|
+
setSubmitting(false);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const update = async (did: string, body: UpdateDomainRequest): Promise<DomainResponse | null> => {
|
|
444
|
+
const current = orgId();
|
|
445
|
+
if (!current || !client?.organizations) return null;
|
|
446
|
+
setSubmitting(true);
|
|
447
|
+
setError(null);
|
|
448
|
+
try {
|
|
449
|
+
const result = await client.organizations.updateDomain(current, did, body);
|
|
450
|
+
await refetch();
|
|
451
|
+
return result;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
454
|
+
return null;
|
|
455
|
+
} finally {
|
|
456
|
+
setSubmitting(false);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const remove = async (did: string): Promise<boolean> => {
|
|
461
|
+
const current = orgId();
|
|
462
|
+
if (!current || !client?.organizations) return false;
|
|
463
|
+
setSubmitting(true);
|
|
464
|
+
setError(null);
|
|
465
|
+
try {
|
|
466
|
+
await client.organizations.deleteDomain(current, did);
|
|
467
|
+
await refetch();
|
|
468
|
+
return true;
|
|
469
|
+
} catch (err) {
|
|
470
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
471
|
+
return false;
|
|
472
|
+
} finally {
|
|
473
|
+
setSubmitting(false);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
createEffect(() => {
|
|
478
|
+
// Re-fetch whenever the org id changes.
|
|
479
|
+
void orgId();
|
|
480
|
+
void refetch();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
domains,
|
|
485
|
+
loading,
|
|
486
|
+
submitting,
|
|
487
|
+
error,
|
|
488
|
+
lastCreated,
|
|
489
|
+
refetch,
|
|
490
|
+
claim,
|
|
491
|
+
verify,
|
|
492
|
+
update,
|
|
493
|
+
remove,
|
|
494
|
+
};
|
|
495
|
+
}
|