@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { CreateDomainResponse } from "@yackey-labs/yauth-client";
|
|
2
|
+
import { type Component, createSignal, Show } from "solid-js";
|
|
3
|
+
import { createDomains } from "../hooks/create-organizations";
|
|
4
|
+
|
|
5
|
+
export interface DomainClaimProps {
|
|
6
|
+
organizationId: string;
|
|
7
|
+
onSuccess?: (result: CreateDomainResponse) => void;
|
|
8
|
+
onError?: (error: Error) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const isDomain = (v: string) => /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(v.trim());
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Form for an org admin to claim a new email domain. On success surfaces
|
|
15
|
+
* the verification token + the DNS record name; the parent UI is expected
|
|
16
|
+
* to chain into <DomainVerifyStep /> for the "publish the TXT record" step.
|
|
17
|
+
*/
|
|
18
|
+
export const DomainClaim: Component<DomainClaimProps> = (props) => {
|
|
19
|
+
const { claim, lastCreated, error, submitting } = createDomains(() => props.organizationId);
|
|
20
|
+
const [domain, setDomain] = createSignal("");
|
|
21
|
+
const [autoJoin, setAutoJoin] = createSignal(false);
|
|
22
|
+
const [requireVerified, setRequireVerified] = createSignal(true);
|
|
23
|
+
|
|
24
|
+
const handleSubmit = async (e: SubmitEvent) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
const trimmed = domain().trim().toLowerCase();
|
|
27
|
+
if (!isDomain(trimmed)) return;
|
|
28
|
+
const result = await claim({
|
|
29
|
+
domain: trimmed,
|
|
30
|
+
auto_join_on_signup: autoJoin(),
|
|
31
|
+
require_email_verified: requireVerified(),
|
|
32
|
+
});
|
|
33
|
+
if (result) {
|
|
34
|
+
setDomain("");
|
|
35
|
+
props.onSuccess?.(result);
|
|
36
|
+
} else if (error()) {
|
|
37
|
+
props.onError?.(new Error(error() as string));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<form class="space-y-4" on:submit={handleSubmit}>
|
|
43
|
+
<Show when={error()}>
|
|
44
|
+
<div
|
|
45
|
+
class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
46
|
+
role="alert"
|
|
47
|
+
aria-live="polite"
|
|
48
|
+
>
|
|
49
|
+
{error()}
|
|
50
|
+
</div>
|
|
51
|
+
</Show>
|
|
52
|
+
|
|
53
|
+
<div class="space-y-2">
|
|
54
|
+
<label class="text-sm font-medium leading-none" for="yauth-domain-input">
|
|
55
|
+
Domain
|
|
56
|
+
</label>
|
|
57
|
+
<input
|
|
58
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
|
59
|
+
id="yauth-domain-input"
|
|
60
|
+
name="domain"
|
|
61
|
+
type="text"
|
|
62
|
+
placeholder="acme.com"
|
|
63
|
+
value={domain()}
|
|
64
|
+
on:input={(e) => setDomain(e.currentTarget.value)}
|
|
65
|
+
required
|
|
66
|
+
autocomplete="off"
|
|
67
|
+
disabled={submitting()}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<label class="flex items-center gap-2 text-sm">
|
|
72
|
+
<input
|
|
73
|
+
type="checkbox"
|
|
74
|
+
checked={autoJoin()}
|
|
75
|
+
on:change={(e) => setAutoJoin(e.currentTarget.checked)}
|
|
76
|
+
disabled={submitting()}
|
|
77
|
+
/>
|
|
78
|
+
Auto-join matching users on signup
|
|
79
|
+
</label>
|
|
80
|
+
|
|
81
|
+
<label class="flex items-center gap-2 text-sm">
|
|
82
|
+
<input
|
|
83
|
+
type="checkbox"
|
|
84
|
+
checked={requireVerified()}
|
|
85
|
+
on:change={(e) => setRequireVerified(e.currentTarget.checked)}
|
|
86
|
+
disabled={submitting()}
|
|
87
|
+
/>
|
|
88
|
+
Require verified email before auto-join
|
|
89
|
+
</label>
|
|
90
|
+
|
|
91
|
+
<button
|
|
92
|
+
class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
93
|
+
type="submit"
|
|
94
|
+
disabled={submitting() || !isDomain(domain().trim())}
|
|
95
|
+
>
|
|
96
|
+
{submitting() ? "Claiming…" : "Claim domain"}
|
|
97
|
+
</button>
|
|
98
|
+
|
|
99
|
+
<Show when={lastCreated()}>
|
|
100
|
+
{(result) => (
|
|
101
|
+
<div class="rounded-md border border-input bg-muted/40 p-3 space-y-2">
|
|
102
|
+
<p class="text-xs text-muted-foreground">
|
|
103
|
+
Publish the following DNS TXT record to verify ownership. The token is shown ONCE —
|
|
104
|
+
copy it now.
|
|
105
|
+
</p>
|
|
106
|
+
<div>
|
|
107
|
+
<span class="text-xs font-medium">Record</span>
|
|
108
|
+
<code class="mt-1 block break-all font-mono text-xs">{result().dns_record_name}</code>
|
|
109
|
+
</div>
|
|
110
|
+
<div>
|
|
111
|
+
<span class="text-xs font-medium">Value</span>
|
|
112
|
+
<code class="mt-1 block break-all font-mono text-xs">
|
|
113
|
+
{result().verification_token}
|
|
114
|
+
</code>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</Show>
|
|
119
|
+
</form>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type Component, For, Show } from "solid-js";
|
|
2
|
+
import { createDomains } from "../hooks/create-organizations";
|
|
3
|
+
|
|
4
|
+
export interface DomainListProps {
|
|
5
|
+
organizationId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const STATUS_LABEL: Record<string, string> = {
|
|
9
|
+
pending: "Pending verification",
|
|
10
|
+
verified: "Verified",
|
|
11
|
+
failed: "Verification failed",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read-only-with-actions listing of an org's claimed domains. Admins get
|
|
16
|
+
* verify / remove buttons; non-admins still see the list (the backend gate
|
|
17
|
+
* is on member-of-org rather than admin-of-org).
|
|
18
|
+
*/
|
|
19
|
+
export const DomainList: Component<DomainListProps> = (props) => {
|
|
20
|
+
const { domains, loading, error, verify, remove, submitting } = createDomains(
|
|
21
|
+
() => props.organizationId,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div class="space-y-3">
|
|
26
|
+
<Show when={error()}>
|
|
27
|
+
<div
|
|
28
|
+
class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
29
|
+
role="alert"
|
|
30
|
+
aria-live="polite"
|
|
31
|
+
>
|
|
32
|
+
{error()}
|
|
33
|
+
</div>
|
|
34
|
+
</Show>
|
|
35
|
+
|
|
36
|
+
<Show when={loading()}>
|
|
37
|
+
<p class="text-sm text-muted-foreground">Loading domains…</p>
|
|
38
|
+
</Show>
|
|
39
|
+
|
|
40
|
+
<Show when={!loading() && domains().length === 0}>
|
|
41
|
+
<p class="text-sm text-muted-foreground">No domains claimed yet.</p>
|
|
42
|
+
</Show>
|
|
43
|
+
|
|
44
|
+
<ul class="divide-y divide-border">
|
|
45
|
+
<For each={domains()}>
|
|
46
|
+
{(d) => (
|
|
47
|
+
<li class="flex items-center justify-between gap-3 py-2">
|
|
48
|
+
<div class="min-w-0 flex-1">
|
|
49
|
+
<div class="flex items-center gap-2">
|
|
50
|
+
<code class="font-mono text-sm">{d.domain}</code>
|
|
51
|
+
<span
|
|
52
|
+
class={`rounded-full px-2 py-0.5 text-xs ${
|
|
53
|
+
d.status === "verified"
|
|
54
|
+
? "bg-emerald-500/10 text-emerald-700"
|
|
55
|
+
: d.status === "failed"
|
|
56
|
+
? "bg-destructive/10 text-destructive"
|
|
57
|
+
: "bg-muted text-muted-foreground"
|
|
58
|
+
}`}
|
|
59
|
+
>
|
|
60
|
+
{STATUS_LABEL[d.status] ?? d.status}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
<p class="text-xs text-muted-foreground">
|
|
64
|
+
Auto-join {d.auto_join_on_signup ? "on" : "off"} · default role{" "}
|
|
65
|
+
{d.default_role_on_auto_join}
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="flex gap-2">
|
|
69
|
+
<Show when={d.status !== "verified"}>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
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"
|
|
73
|
+
on:click={() => verify(d.id)}
|
|
74
|
+
disabled={submitting()}
|
|
75
|
+
>
|
|
76
|
+
Verify
|
|
77
|
+
</button>
|
|
78
|
+
</Show>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-transparent px-3 text-xs font-medium text-destructive shadow-sm transition-colors hover:bg-destructive/10 disabled:pointer-events-none disabled:opacity-50"
|
|
82
|
+
on:click={() => remove(d.id)}
|
|
83
|
+
disabled={submitting()}
|
|
84
|
+
>
|
|
85
|
+
Remove
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</li>
|
|
89
|
+
)}
|
|
90
|
+
</For>
|
|
91
|
+
</ul>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CreateDomainResponse, VerifyDomainResponse } from "@yackey-labs/yauth-client";
|
|
2
|
+
import { type Component, createSignal, Show } from "solid-js";
|
|
3
|
+
import { createDomains } from "../hooks/create-organizations";
|
|
4
|
+
|
|
5
|
+
export interface DomainVerifyStepProps {
|
|
6
|
+
organizationId: string;
|
|
7
|
+
/** The domain claim to verify — usually the result of `<DomainClaim />`. */
|
|
8
|
+
claim: CreateDomainResponse;
|
|
9
|
+
onVerified?: (result: VerifyDomainResponse) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Final step of the domain-onboarding flow. Displays the DNS TXT record the
|
|
14
|
+
* operator must publish, plus a "Run verification" button that hits the
|
|
15
|
+
* verify endpoint. The verification is best-effort — operators can retry.
|
|
16
|
+
*/
|
|
17
|
+
export const DomainVerifyStep: Component<DomainVerifyStepProps> = (props) => {
|
|
18
|
+
const { verify, error, submitting } = createDomains(() => props.organizationId);
|
|
19
|
+
const [lastResult, setLastResult] = createSignal<VerifyDomainResponse | null>(null);
|
|
20
|
+
|
|
21
|
+
const handleVerify = async () => {
|
|
22
|
+
const result = await verify(props.claim.domain.id);
|
|
23
|
+
setLastResult(result);
|
|
24
|
+
if (result?.verified) {
|
|
25
|
+
props.onVerified?.(result);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div class="space-y-3">
|
|
31
|
+
<div class="rounded-md border border-input bg-muted/40 p-3 space-y-2">
|
|
32
|
+
<p class="text-xs text-muted-foreground">
|
|
33
|
+
Publish the following DNS TXT record at your DNS provider, then click "Run verification".
|
|
34
|
+
Some DNS providers can take up to 60 seconds to propagate.
|
|
35
|
+
</p>
|
|
36
|
+
<div>
|
|
37
|
+
<span class="text-xs font-medium">Record</span>
|
|
38
|
+
<code class="mt-1 block break-all font-mono text-xs">{props.claim.dns_record_name}</code>
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
<span class="text-xs font-medium">Value</span>
|
|
42
|
+
<code class="mt-1 block break-all font-mono text-xs">
|
|
43
|
+
{props.claim.verification_token}
|
|
44
|
+
</code>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<Show when={error()}>
|
|
49
|
+
<div
|
|
50
|
+
class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
51
|
+
role="alert"
|
|
52
|
+
aria-live="polite"
|
|
53
|
+
>
|
|
54
|
+
{error()}
|
|
55
|
+
</div>
|
|
56
|
+
</Show>
|
|
57
|
+
|
|
58
|
+
<Show when={lastResult() && !lastResult()?.verified}>
|
|
59
|
+
<div class="rounded-md bg-amber-500/10 px-3 py-2 text-sm text-amber-700">
|
|
60
|
+
Verification did not find the expected DNS TXT record. Double-check the record name +
|
|
61
|
+
value, then retry.
|
|
62
|
+
</div>
|
|
63
|
+
</Show>
|
|
64
|
+
|
|
65
|
+
<Show when={lastResult()?.verified}>
|
|
66
|
+
<div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
|
67
|
+
Domain verified. JIT auto-join is now active for matching users.
|
|
68
|
+
</div>
|
|
69
|
+
</Show>
|
|
70
|
+
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
|
|
74
|
+
on:click={handleVerify}
|
|
75
|
+
disabled={submitting()}
|
|
76
|
+
>
|
|
77
|
+
{submitting() ? "Verifying…" : "Run verification"}
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { MembershipResponse } from "@yackey-labs/yauth-client";
|
|
2
|
+
import { type Component, createSignal } from "solid-js";
|
|
3
|
+
import { Show } from "solid-js/web";
|
|
4
|
+
import { createInvitation } from "../hooks/create-organizations";
|
|
5
|
+
|
|
6
|
+
export interface InvitationAcceptProps {
|
|
7
|
+
/** Pre-fill the token (e.g. from a query string). */
|
|
8
|
+
token?: string;
|
|
9
|
+
onSuccess?: (membership: MembershipResponse) => void;
|
|
10
|
+
onError?: (error: Error) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const InvitationAccept: Component<InvitationAcceptProps> = (props) => {
|
|
14
|
+
const { accept, submitting, error } = createInvitation();
|
|
15
|
+
const [token, setToken] = createSignal(props.token ?? "");
|
|
16
|
+
const [accepted, setAccepted] = createSignal<MembershipResponse | null>(null);
|
|
17
|
+
|
|
18
|
+
const handleSubmit = async (e: SubmitEvent) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
const result = await accept(token().trim());
|
|
21
|
+
if (result) {
|
|
22
|
+
setAccepted(result);
|
|
23
|
+
props.onSuccess?.(result);
|
|
24
|
+
} else if (error()) {
|
|
25
|
+
props.onError?.(new Error(error() as string));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<form class="space-y-4" on:submit={handleSubmit}>
|
|
31
|
+
<Show when={error()}>
|
|
32
|
+
<div
|
|
33
|
+
class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
34
|
+
role="alert"
|
|
35
|
+
aria-live="polite"
|
|
36
|
+
>
|
|
37
|
+
{error()}
|
|
38
|
+
</div>
|
|
39
|
+
</Show>
|
|
40
|
+
|
|
41
|
+
<Show when={accepted()}>
|
|
42
|
+
<div
|
|
43
|
+
class="rounded-md bg-primary/10 px-3 py-2 text-sm"
|
|
44
|
+
role="status"
|
|
45
|
+
aria-live="polite"
|
|
46
|
+
>
|
|
47
|
+
Invitation accepted. Welcome to the organization!
|
|
48
|
+
</div>
|
|
49
|
+
</Show>
|
|
50
|
+
|
|
51
|
+
<Show when={!accepted()}>
|
|
52
|
+
<div class="space-y-2">
|
|
53
|
+
<label
|
|
54
|
+
class="text-sm font-medium leading-none"
|
|
55
|
+
for="yauth-invitation-accept-token"
|
|
56
|
+
>
|
|
57
|
+
Invitation token
|
|
58
|
+
</label>
|
|
59
|
+
<input
|
|
60
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 font-mono text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
|
61
|
+
id="yauth-invitation-accept-token"
|
|
62
|
+
name="token"
|
|
63
|
+
type="text"
|
|
64
|
+
value={token()}
|
|
65
|
+
on:input={(e) => setToken(e.currentTarget.value)}
|
|
66
|
+
required
|
|
67
|
+
autocomplete="off"
|
|
68
|
+
disabled={submitting()}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<button
|
|
73
|
+
class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
74
|
+
type="submit"
|
|
75
|
+
disabled={submitting() || !token().trim()}
|
|
76
|
+
>
|
|
77
|
+
{submitting() ? "Accepting…" : "Accept invitation"}
|
|
78
|
+
</button>
|
|
79
|
+
</Show>
|
|
80
|
+
</form>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { CreateInvitationResponse } from "@yackey-labs/yauth-client";
|
|
2
|
+
import { type Component, createSignal } from "solid-js";
|
|
3
|
+
import { Show } from "solid-js/web";
|
|
4
|
+
import { createMembers } from "../hooks/create-organizations";
|
|
5
|
+
|
|
6
|
+
export interface InviteFormProps {
|
|
7
|
+
organizationId: string;
|
|
8
|
+
onSuccess?: (result: CreateInvitationResponse) => void;
|
|
9
|
+
onError?: (error: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const isEmail = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
13
|
+
|
|
14
|
+
export const InviteForm: Component<InviteFormProps> = (props) => {
|
|
15
|
+
const { invite, error } = createMembers(() => props.organizationId);
|
|
16
|
+
|
|
17
|
+
const [email, setEmail] = createSignal("");
|
|
18
|
+
const [role, setRole] = createSignal("member");
|
|
19
|
+
const [submitting, setSubmitting] = createSignal(false);
|
|
20
|
+
const [lastToken, setLastToken] = createSignal<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const handleSubmit = async (e: SubmitEvent) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
const trimmed = email().trim();
|
|
25
|
+
if (!isEmail(trimmed)) return;
|
|
26
|
+
setSubmitting(true);
|
|
27
|
+
const result = await invite({ email: trimmed, role: role() });
|
|
28
|
+
setSubmitting(false);
|
|
29
|
+
if (result) {
|
|
30
|
+
setLastToken(result.token);
|
|
31
|
+
setEmail("");
|
|
32
|
+
props.onSuccess?.(result);
|
|
33
|
+
} else if (error()) {
|
|
34
|
+
props.onError?.(new Error(error() as string));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<form class="space-y-4" on:submit={handleSubmit}>
|
|
40
|
+
<Show when={error()}>
|
|
41
|
+
<div
|
|
42
|
+
class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
43
|
+
role="alert"
|
|
44
|
+
aria-live="polite"
|
|
45
|
+
>
|
|
46
|
+
{error()}
|
|
47
|
+
</div>
|
|
48
|
+
</Show>
|
|
49
|
+
|
|
50
|
+
<div class="space-y-2">
|
|
51
|
+
<label
|
|
52
|
+
class="text-sm font-medium leading-none"
|
|
53
|
+
for="yauth-invite-email"
|
|
54
|
+
>
|
|
55
|
+
Email
|
|
56
|
+
</label>
|
|
57
|
+
<input
|
|
58
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
|
59
|
+
id="yauth-invite-email"
|
|
60
|
+
name="email"
|
|
61
|
+
type="email"
|
|
62
|
+
value={email()}
|
|
63
|
+
on:input={(e) => setEmail(e.currentTarget.value)}
|
|
64
|
+
required
|
|
65
|
+
autocomplete="email"
|
|
66
|
+
disabled={submitting()}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="space-y-2">
|
|
71
|
+
<label class="text-sm font-medium leading-none" for="yauth-invite-role">
|
|
72
|
+
Role
|
|
73
|
+
</label>
|
|
74
|
+
<select
|
|
75
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
|
76
|
+
id="yauth-invite-role"
|
|
77
|
+
name="role"
|
|
78
|
+
value={role()}
|
|
79
|
+
on:change={(e) => setRole(e.currentTarget.value)}
|
|
80
|
+
disabled={submitting()}
|
|
81
|
+
>
|
|
82
|
+
<option value="member">Member</option>
|
|
83
|
+
<option value="admin">Admin</option>
|
|
84
|
+
</select>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<button
|
|
88
|
+
class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
89
|
+
type="submit"
|
|
90
|
+
disabled={submitting() || !isEmail(email().trim())}
|
|
91
|
+
>
|
|
92
|
+
{submitting() ? "Sending…" : "Send invitation"}
|
|
93
|
+
</button>
|
|
94
|
+
|
|
95
|
+
<Show when={lastToken()}>
|
|
96
|
+
<div class="rounded-md border border-input bg-muted/40 p-3">
|
|
97
|
+
<p class="text-xs text-muted-foreground">
|
|
98
|
+
Invitation created. Deliver this token to the invitee — it will
|
|
99
|
+
not be shown again.
|
|
100
|
+
</p>
|
|
101
|
+
<code class="mt-2 block break-all font-mono text-xs">
|
|
102
|
+
{lastToken()}
|
|
103
|
+
</code>
|
|
104
|
+
</div>
|
|
105
|
+
</Show>
|
|
106
|
+
</form>
|
|
107
|
+
);
|
|
108
|
+
};
|