@pylonsync/client 0.3.267
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/README.md +125 -0
- package/package.json +32 -0
- package/src/components/AcceptInvite.tsx +160 -0
- package/src/components/ChatBot.tsx +228 -0
- package/src/components/ConnectAccount.tsx +119 -0
- package/src/components/EnsureGuest.tsx +49 -0
- package/src/components/EntityForm.tsx +308 -0
- package/src/components/EntityList.tsx +203 -0
- package/src/components/FileUpload.tsx +213 -0
- package/src/components/Gates.tsx +139 -0
- package/src/components/InviteMembers.tsx +562 -0
- package/src/components/OrganizationSwitcher.tsx +417 -0
- package/src/components/PasswordReset.tsx +302 -0
- package/src/components/SignIn.tsx +515 -0
- package/src/components/SignOutButton.tsx +42 -0
- package/src/components/UserButton.tsx +163 -0
- package/src/components/UserProfile.tsx +485 -0
- package/src/hooks/useAuth.ts +27 -0
- package/src/index.ts +130 -0
- package/src/lib/api.ts +368 -0
- package/src/lib/cn.ts +7 -0
- package/src/router/Router.tsx +282 -0
- package/src/router/context.ts +25 -0
- package/src/router/match.ts +106 -0
- package/src/router/useRouter.ts +40 -0
- package/src/theme.css +30 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type FormEvent,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { useAuth } from "../hooks/useAuth";
|
|
12
|
+
import {
|
|
13
|
+
type ActiveSession,
|
|
14
|
+
type ApiKeyCreated,
|
|
15
|
+
ApiError,
|
|
16
|
+
type ApiKeySummary,
|
|
17
|
+
changePassword,
|
|
18
|
+
createApiKey,
|
|
19
|
+
listActiveSessions,
|
|
20
|
+
listApiKeys,
|
|
21
|
+
revokeAllSessions,
|
|
22
|
+
revokeApiKey,
|
|
23
|
+
} from "../lib/api";
|
|
24
|
+
import { cn } from "../lib/cn";
|
|
25
|
+
|
|
26
|
+
export interface UserProfileProps {
|
|
27
|
+
/** Hide sections you don't need. */
|
|
28
|
+
hidePassword?: boolean;
|
|
29
|
+
hideSessions?: boolean;
|
|
30
|
+
hideApiKeys?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Drop-in account management surface — Clerk's `<UserProfile />` shape.
|
|
36
|
+
* Sections:
|
|
37
|
+
* - Identity card (user_id, active org)
|
|
38
|
+
* - Password change (skipped for OAuth-only accounts)
|
|
39
|
+
* - Active sessions + "Sign out other devices"
|
|
40
|
+
* - API keys (create / list / revoke)
|
|
41
|
+
*
|
|
42
|
+
* Each section can be hidden via props if the consuming app handles it
|
|
43
|
+
* elsewhere. Renders nothing for signed-out users.
|
|
44
|
+
*/
|
|
45
|
+
export function UserProfile({
|
|
46
|
+
hidePassword,
|
|
47
|
+
hideSessions,
|
|
48
|
+
hideApiKeys,
|
|
49
|
+
className,
|
|
50
|
+
}: UserProfileProps) {
|
|
51
|
+
const { isSignedIn, userId, tenantId } = useAuth();
|
|
52
|
+
if (!isSignedIn) return null;
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
"mx-auto w-full max-w-xl space-y-6 rounded-xl border border-[var(--pylon-rule,#e5e7eb)] bg-[var(--pylon-paper,#ffffff)] p-7 shadow-sm",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
<IdentityCard userId={userId} tenantId={tenantId} />
|
|
61
|
+
{!hidePassword ? <PasswordChange /> : null}
|
|
62
|
+
{!hideSessions ? <Sessions /> : null}
|
|
63
|
+
{!hideApiKeys ? <ApiKeys /> : null}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Identity
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function IdentityCard({
|
|
73
|
+
userId,
|
|
74
|
+
tenantId,
|
|
75
|
+
}: {
|
|
76
|
+
userId: string | null;
|
|
77
|
+
tenantId: string | null;
|
|
78
|
+
}) {
|
|
79
|
+
return (
|
|
80
|
+
<section className="space-y-1">
|
|
81
|
+
<SectionLabel>Account</SectionLabel>
|
|
82
|
+
<div className="rounded-md border border-[var(--pylon-rule,#e5e7eb)] bg-[var(--pylon-paper-2,#f4f4f5)] p-3">
|
|
83
|
+
<p className="text-xs text-[var(--pylon-ink-3,#71717a)]">User ID</p>
|
|
84
|
+
<p className="break-all font-mono text-sm text-[var(--pylon-ink,#0a0a0a)]">
|
|
85
|
+
{userId ?? "—"}
|
|
86
|
+
</p>
|
|
87
|
+
{tenantId ? (
|
|
88
|
+
<>
|
|
89
|
+
<p className="mt-2 text-xs text-[var(--pylon-ink-3,#71717a)]">
|
|
90
|
+
Active org
|
|
91
|
+
</p>
|
|
92
|
+
<p className="break-all font-mono text-sm text-[var(--pylon-ink,#0a0a0a)]">
|
|
93
|
+
{tenantId}
|
|
94
|
+
</p>
|
|
95
|
+
</>
|
|
96
|
+
) : null}
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Password change
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function PasswordChange() {
|
|
107
|
+
const [current, setCurrent] = useState("");
|
|
108
|
+
const [next, setNext] = useState("");
|
|
109
|
+
const [confirm, setConfirm] = useState("");
|
|
110
|
+
const [pending, setPending] = useState(false);
|
|
111
|
+
const [status, setStatus] =
|
|
112
|
+
useState<{ ok: boolean; message: string } | null>(null);
|
|
113
|
+
|
|
114
|
+
async function onSubmit(e: FormEvent) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
setStatus(null);
|
|
117
|
+
if (next !== confirm) {
|
|
118
|
+
setStatus({ ok: false, message: "New passwords don't match." });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
setPending(true);
|
|
122
|
+
try {
|
|
123
|
+
await changePassword({ currentPassword: current, newPassword: next });
|
|
124
|
+
setStatus({ ok: true, message: "Password updated." });
|
|
125
|
+
setCurrent("");
|
|
126
|
+
setNext("");
|
|
127
|
+
setConfirm("");
|
|
128
|
+
} catch (err) {
|
|
129
|
+
setStatus({ ok: false, message: messageFromError(err) });
|
|
130
|
+
} finally {
|
|
131
|
+
setPending(false);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<section className="space-y-3">
|
|
137
|
+
<SectionLabel>Change password</SectionLabel>
|
|
138
|
+
<form onSubmit={onSubmit} className="space-y-2">
|
|
139
|
+
<PasswordField
|
|
140
|
+
label="Current"
|
|
141
|
+
value={current}
|
|
142
|
+
onChange={setCurrent}
|
|
143
|
+
autoComplete="current-password"
|
|
144
|
+
/>
|
|
145
|
+
<PasswordField
|
|
146
|
+
label="New"
|
|
147
|
+
value={next}
|
|
148
|
+
onChange={setNext}
|
|
149
|
+
autoComplete="new-password"
|
|
150
|
+
/>
|
|
151
|
+
<PasswordField
|
|
152
|
+
label="Confirm"
|
|
153
|
+
value={confirm}
|
|
154
|
+
onChange={setConfirm}
|
|
155
|
+
autoComplete="new-password"
|
|
156
|
+
/>
|
|
157
|
+
<button
|
|
158
|
+
type="submit"
|
|
159
|
+
disabled={pending || !current || !next}
|
|
160
|
+
className="w-full rounded-md bg-[var(--pylon-ink,#0a0a0a)] px-3 py-2 text-sm font-medium text-[var(--pylon-paper,#ffffff)] transition-opacity hover:opacity-90 disabled:opacity-60"
|
|
161
|
+
>
|
|
162
|
+
{pending ? "…" : "Update password"}
|
|
163
|
+
</button>
|
|
164
|
+
{status ? (
|
|
165
|
+
<p
|
|
166
|
+
className={cn(
|
|
167
|
+
"rounded-md border px-3 py-2 text-xs",
|
|
168
|
+
status.ok
|
|
169
|
+
? "border-[var(--pylon-rule,#e5e7eb)] bg-[var(--pylon-paper-2,#f4f4f5)] text-[var(--pylon-ink-2,#52525b)]"
|
|
170
|
+
: "border-[var(--pylon-error-rule,#fecaca)] bg-[var(--pylon-error-bg,#fef2f2)] text-[var(--pylon-error-ink,#b91c1c)]",
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
{status.message}
|
|
174
|
+
</p>
|
|
175
|
+
) : null}
|
|
176
|
+
</form>
|
|
177
|
+
</section>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function PasswordField({
|
|
182
|
+
label,
|
|
183
|
+
value,
|
|
184
|
+
onChange,
|
|
185
|
+
autoComplete,
|
|
186
|
+
}: {
|
|
187
|
+
label: string;
|
|
188
|
+
value: string;
|
|
189
|
+
onChange: (v: string) => void;
|
|
190
|
+
autoComplete: string;
|
|
191
|
+
}) {
|
|
192
|
+
return (
|
|
193
|
+
<label className="block space-y-1">
|
|
194
|
+
<span className="text-xs font-medium text-[var(--pylon-ink-2,#52525b)]">
|
|
195
|
+
{label}
|
|
196
|
+
</span>
|
|
197
|
+
<input
|
|
198
|
+
type="password"
|
|
199
|
+
value={value}
|
|
200
|
+
onChange={(e) => onChange(e.target.value)}
|
|
201
|
+
autoComplete={autoComplete}
|
|
202
|
+
className="w-full rounded-md border border-[var(--pylon-rule,#e5e7eb)] bg-[var(--pylon-paper,#ffffff)] px-3 py-2 text-sm text-[var(--pylon-ink,#0a0a0a)] focus:border-[var(--pylon-ink,#0a0a0a)] focus:outline-none"
|
|
203
|
+
/>
|
|
204
|
+
</label>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Sessions
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
function Sessions() {
|
|
213
|
+
const [sessions, setSessions] = useState<ActiveSession[] | null>(null);
|
|
214
|
+
const [pending, setPending] = useState(false);
|
|
215
|
+
const [error, setError] = useState<string | null>(null);
|
|
216
|
+
|
|
217
|
+
const refresh = useCallback(async () => {
|
|
218
|
+
setSessions(await listActiveSessions());
|
|
219
|
+
}, []);
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
void refresh();
|
|
223
|
+
}, [refresh]);
|
|
224
|
+
|
|
225
|
+
async function onSignOutOthers() {
|
|
226
|
+
setError(null);
|
|
227
|
+
setPending(true);
|
|
228
|
+
try {
|
|
229
|
+
await revokeAllSessions();
|
|
230
|
+
void refresh();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
setError(messageFromError(err));
|
|
233
|
+
} finally {
|
|
234
|
+
setPending(false);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<section className="space-y-3">
|
|
240
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
241
|
+
<SectionLabel>Active sessions</SectionLabel>
|
|
242
|
+
{sessions && sessions.length > 1 ? (
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={onSignOutOthers}
|
|
246
|
+
disabled={pending}
|
|
247
|
+
className="text-xs font-medium text-[var(--pylon-error-ink,#b91c1c)] hover:underline disabled:opacity-50"
|
|
248
|
+
>
|
|
249
|
+
Sign out all devices
|
|
250
|
+
</button>
|
|
251
|
+
) : null}
|
|
252
|
+
</div>
|
|
253
|
+
{!sessions ? (
|
|
254
|
+
<p className="text-xs text-[var(--pylon-ink-3,#71717a)]">
|
|
255
|
+
Loading…
|
|
256
|
+
</p>
|
|
257
|
+
) : sessions.length === 0 ? (
|
|
258
|
+
<p className="text-xs text-[var(--pylon-ink-3,#71717a)]">
|
|
259
|
+
No active sessions.
|
|
260
|
+
</p>
|
|
261
|
+
) : (
|
|
262
|
+
<ul className="divide-y divide-[var(--pylon-rule,#e5e7eb)] rounded-md border border-[var(--pylon-rule,#e5e7eb)]">
|
|
263
|
+
{sessions.map((s) => (
|
|
264
|
+
<li
|
|
265
|
+
key={s.token_prefix + s.created_at}
|
|
266
|
+
className="flex items-center justify-between gap-2 px-3 py-2 text-sm"
|
|
267
|
+
>
|
|
268
|
+
<span className="min-w-0">
|
|
269
|
+
<span className="block truncate font-medium text-[var(--pylon-ink,#0a0a0a)]">
|
|
270
|
+
{s.device || "Unknown device"}
|
|
271
|
+
</span>
|
|
272
|
+
<span className="block text-[11px] text-[var(--pylon-ink-3,#71717a)]">
|
|
273
|
+
Token …{s.token_prefix} · expires{" "}
|
|
274
|
+
{formatRelative(s.expires_at)}
|
|
275
|
+
</span>
|
|
276
|
+
</span>
|
|
277
|
+
</li>
|
|
278
|
+
))}
|
|
279
|
+
</ul>
|
|
280
|
+
)}
|
|
281
|
+
{error ? <ErrorBanner message={error} /> : null}
|
|
282
|
+
</section>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// API keys
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
function ApiKeys() {
|
|
291
|
+
const [keys, setKeys] = useState<ApiKeySummary[] | null>(null);
|
|
292
|
+
const [creating, setCreating] = useState(false);
|
|
293
|
+
const [newName, setNewName] = useState("");
|
|
294
|
+
const [lastCreated, setLastCreated] = useState<ApiKeyCreated | null>(null);
|
|
295
|
+
const [pending, setPending] = useState(false);
|
|
296
|
+
const [error, setError] = useState<string | null>(null);
|
|
297
|
+
|
|
298
|
+
const refresh = useCallback(async () => {
|
|
299
|
+
setKeys(await listApiKeys());
|
|
300
|
+
}, []);
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
void refresh();
|
|
304
|
+
}, [refresh]);
|
|
305
|
+
|
|
306
|
+
async function onCreate(e: FormEvent) {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
setError(null);
|
|
309
|
+
setPending(true);
|
|
310
|
+
try {
|
|
311
|
+
const created = await createApiKey({ name: newName.trim() });
|
|
312
|
+
setLastCreated(created);
|
|
313
|
+
setNewName("");
|
|
314
|
+
setCreating(false);
|
|
315
|
+
void refresh();
|
|
316
|
+
} catch (err) {
|
|
317
|
+
setError(messageFromError(err));
|
|
318
|
+
} finally {
|
|
319
|
+
setPending(false);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function onRevoke(id: string) {
|
|
324
|
+
setError(null);
|
|
325
|
+
try {
|
|
326
|
+
await revokeApiKey(id);
|
|
327
|
+
void refresh();
|
|
328
|
+
} catch (err) {
|
|
329
|
+
setError(messageFromError(err));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<section className="space-y-3">
|
|
335
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
336
|
+
<SectionLabel>API keys</SectionLabel>
|
|
337
|
+
{!creating ? (
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
onClick={() => setCreating(true)}
|
|
341
|
+
className="text-xs font-medium text-[var(--pylon-ink,#0a0a0a)] hover:underline"
|
|
342
|
+
>
|
|
343
|
+
New key
|
|
344
|
+
</button>
|
|
345
|
+
) : null}
|
|
346
|
+
</div>
|
|
347
|
+
{creating ? (
|
|
348
|
+
<form onSubmit={onCreate} className="flex gap-2">
|
|
349
|
+
<input
|
|
350
|
+
value={newName}
|
|
351
|
+
onChange={(e) => setNewName(e.target.value)}
|
|
352
|
+
placeholder="Key name"
|
|
353
|
+
required
|
|
354
|
+
autoFocus
|
|
355
|
+
className="flex-1 rounded-md border border-[var(--pylon-rule,#e5e7eb)] bg-[var(--pylon-paper,#ffffff)] px-3 py-2 text-sm text-[var(--pylon-ink,#0a0a0a)] focus:border-[var(--pylon-ink,#0a0a0a)] focus:outline-none"
|
|
356
|
+
/>
|
|
357
|
+
<button
|
|
358
|
+
type="submit"
|
|
359
|
+
disabled={pending || !newName.trim()}
|
|
360
|
+
className="rounded-md bg-[var(--pylon-ink,#0a0a0a)] px-3 py-2 text-sm font-medium text-[var(--pylon-paper,#ffffff)] transition-opacity hover:opacity-90 disabled:opacity-60"
|
|
361
|
+
>
|
|
362
|
+
{pending ? "…" : "Create"}
|
|
363
|
+
</button>
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
onClick={() => setCreating(false)}
|
|
367
|
+
className="rounded-md px-3 py-2 text-xs text-[var(--pylon-ink-2,#52525b)] hover:underline"
|
|
368
|
+
>
|
|
369
|
+
Cancel
|
|
370
|
+
</button>
|
|
371
|
+
</form>
|
|
372
|
+
) : null}
|
|
373
|
+
{lastCreated ? (
|
|
374
|
+
<div className="space-y-1.5 rounded-md border border-[var(--pylon-rule,#e5e7eb)] bg-[var(--pylon-paper-2,#f4f4f5)] p-3">
|
|
375
|
+
<p className="text-[11px] font-medium uppercase tracking-wider text-[var(--pylon-ink-2,#52525b)]">
|
|
376
|
+
Copy now — won't be shown again
|
|
377
|
+
</p>
|
|
378
|
+
<code className="block break-all rounded bg-[var(--pylon-paper,#ffffff)] px-2 py-1 font-mono text-xs text-[var(--pylon-ink,#0a0a0a)]">
|
|
379
|
+
{lastCreated.key}
|
|
380
|
+
</code>
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
onClick={() => setLastCreated(null)}
|
|
384
|
+
className="text-[11px] text-[var(--pylon-ink-3,#71717a)] hover:underline"
|
|
385
|
+
>
|
|
386
|
+
Dismiss
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
) : null}
|
|
390
|
+
{!keys ? (
|
|
391
|
+
<p className="text-xs text-[var(--pylon-ink-3,#71717a)]">
|
|
392
|
+
Loading…
|
|
393
|
+
</p>
|
|
394
|
+
) : keys.length === 0 ? (
|
|
395
|
+
<p className="text-xs text-[var(--pylon-ink-3,#71717a)]">
|
|
396
|
+
No keys yet.
|
|
397
|
+
</p>
|
|
398
|
+
) : (
|
|
399
|
+
<ul className="divide-y divide-[var(--pylon-rule,#e5e7eb)] rounded-md border border-[var(--pylon-rule,#e5e7eb)]">
|
|
400
|
+
{keys.map((k) => (
|
|
401
|
+
<li
|
|
402
|
+
key={k.id}
|
|
403
|
+
className="flex items-center justify-between gap-2 px-3 py-2 text-sm"
|
|
404
|
+
>
|
|
405
|
+
<span className="min-w-0">
|
|
406
|
+
<span className="block truncate font-medium text-[var(--pylon-ink,#0a0a0a)]">
|
|
407
|
+
{k.name}
|
|
408
|
+
</span>
|
|
409
|
+
<span className="block text-[11px] text-[var(--pylon-ink-3,#71717a)]">
|
|
410
|
+
{k.prefix}… · last used{" "}
|
|
411
|
+
{k.last_used_at ? formatRelative(k.last_used_at) : "never"}
|
|
412
|
+
</span>
|
|
413
|
+
</span>
|
|
414
|
+
<button
|
|
415
|
+
type="button"
|
|
416
|
+
onClick={() => onRevoke(k.id)}
|
|
417
|
+
className="rounded-md px-2 py-1 text-xs text-[var(--pylon-error-ink,#b91c1c)] transition-colors hover:bg-[var(--pylon-error-bg,#fef2f2)]"
|
|
418
|
+
>
|
|
419
|
+
Revoke
|
|
420
|
+
</button>
|
|
421
|
+
</li>
|
|
422
|
+
))}
|
|
423
|
+
</ul>
|
|
424
|
+
)}
|
|
425
|
+
{error ? <ErrorBanner message={error} /> : null}
|
|
426
|
+
</section>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Shared helpers
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
function SectionLabel({ children }: { children: ReactNode }) {
|
|
435
|
+
return (
|
|
436
|
+
<p className="text-xs font-medium uppercase tracking-wider text-[var(--pylon-ink-2,#52525b)]">
|
|
437
|
+
{children}
|
|
438
|
+
</p>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function ErrorBanner({ message }: { message: string }) {
|
|
443
|
+
return (
|
|
444
|
+
<p className="rounded-md border border-[var(--pylon-error-rule,#fecaca)] bg-[var(--pylon-error-bg,#fef2f2)] px-3 py-2 text-xs text-[var(--pylon-error-ink,#b91c1c)]">
|
|
445
|
+
{message}
|
|
446
|
+
</p>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function formatRelative(unixSecs: number): string {
|
|
451
|
+
if (!unixSecs) return "—";
|
|
452
|
+
const ms = unixSecs * 1000;
|
|
453
|
+
const diff = ms - Date.now();
|
|
454
|
+
const abs = Math.abs(diff);
|
|
455
|
+
const day = 86_400_000;
|
|
456
|
+
const hour = 3_600_000;
|
|
457
|
+
const min = 60_000;
|
|
458
|
+
const sign = diff < 0 ? "ago" : "from now";
|
|
459
|
+
if (abs > day) return `${Math.round(abs / day)}d ${sign}`;
|
|
460
|
+
if (abs > hour) return `${Math.round(abs / hour)}h ${sign}`;
|
|
461
|
+
if (abs > min) return `${Math.round(abs / min)}m ${sign}`;
|
|
462
|
+
return diff < 0 ? "just now" : "soon";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function messageFromError(err: unknown): string {
|
|
466
|
+
if (err instanceof ApiError) {
|
|
467
|
+
switch (err.code) {
|
|
468
|
+
case "INVALID_CREDENTIALS":
|
|
469
|
+
case "BAD_CURRENT_PASSWORD":
|
|
470
|
+
return "Current password is wrong.";
|
|
471
|
+
case "WEAK_PASSWORD":
|
|
472
|
+
return "Pick a stronger password.";
|
|
473
|
+
case "MISSING_PASSWORD":
|
|
474
|
+
return "Enter a new password.";
|
|
475
|
+
case "API_KEY_AUTH_FORBIDDEN":
|
|
476
|
+
return "This action needs a session, not an API key.";
|
|
477
|
+
case "AUTH_REQUIRED":
|
|
478
|
+
return "Sign in to manage your account.";
|
|
479
|
+
default:
|
|
480
|
+
return err.message;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (err instanceof Error) return err.message;
|
|
484
|
+
return "Something went wrong.";
|
|
485
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { db, useSession } from "@pylonsync/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Drop-in hook that wraps `useSession(db.sync)` so components in
|
|
7
|
+
* `@pylonsync/client` don't have to thread a SyncEngine through props.
|
|
8
|
+
* Apps that init() the global engine (the common case) get a Clerk-style
|
|
9
|
+
* `const { isSignedIn, user } = useAuth()` surface.
|
|
10
|
+
*/
|
|
11
|
+
export function useAuth() {
|
|
12
|
+
const session = useSession(db.sync);
|
|
13
|
+
return {
|
|
14
|
+
isSignedIn: session.isAuthenticated,
|
|
15
|
+
isLoaded: true,
|
|
16
|
+
userId: session.userId,
|
|
17
|
+
tenantId: session.tenantId,
|
|
18
|
+
isAdmin: session.isAdmin,
|
|
19
|
+
session: session.session,
|
|
20
|
+
signOut: session.signOut,
|
|
21
|
+
selectOrg: session.selectOrg,
|
|
22
|
+
clearOrg: session.clearOrg,
|
|
23
|
+
refresh: session.refresh,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type UseAuthReturn = ReturnType<typeof useAuth>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export { SignIn, SignUp } from "./components/SignIn";
|
|
2
|
+
export type { SignInProps, SignUpProps } from "./components/SignIn";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
SignedIn,
|
|
6
|
+
SignedOut,
|
|
7
|
+
Protect,
|
|
8
|
+
HasRole,
|
|
9
|
+
InOrg,
|
|
10
|
+
NoOrg,
|
|
11
|
+
RedirectToSignIn,
|
|
12
|
+
} from "./components/Gates";
|
|
13
|
+
export type {
|
|
14
|
+
ProtectProps,
|
|
15
|
+
HasRoleProps,
|
|
16
|
+
RedirectToSignInProps,
|
|
17
|
+
} from "./components/Gates";
|
|
18
|
+
|
|
19
|
+
export { UserButton } from "./components/UserButton";
|
|
20
|
+
export type { UserButtonProps } from "./components/UserButton";
|
|
21
|
+
|
|
22
|
+
export { SignOutButton } from "./components/SignOutButton";
|
|
23
|
+
export type { SignOutButtonProps } from "./components/SignOutButton";
|
|
24
|
+
|
|
25
|
+
export { EnsureGuest } from "./components/EnsureGuest";
|
|
26
|
+
export type { EnsureGuestProps } from "./components/EnsureGuest";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
OrganizationSwitcher,
|
|
30
|
+
CreateOrganization,
|
|
31
|
+
} from "./components/OrganizationSwitcher";
|
|
32
|
+
export type {
|
|
33
|
+
OrganizationSwitcherProps,
|
|
34
|
+
CreateOrganizationProps,
|
|
35
|
+
} from "./components/OrganizationSwitcher";
|
|
36
|
+
|
|
37
|
+
export { InviteMembers } from "./components/InviteMembers";
|
|
38
|
+
export type { InviteMembersProps } from "./components/InviteMembers";
|
|
39
|
+
|
|
40
|
+
export { AcceptInvite } from "./components/AcceptInvite";
|
|
41
|
+
export type { AcceptInviteProps } from "./components/AcceptInvite";
|
|
42
|
+
|
|
43
|
+
export { ConnectAccount } from "./components/ConnectAccount";
|
|
44
|
+
export type { ConnectAccountProps } from "./components/ConnectAccount";
|
|
45
|
+
|
|
46
|
+
export { FileUpload } from "./components/FileUpload";
|
|
47
|
+
export type {
|
|
48
|
+
FileUploadProps,
|
|
49
|
+
FileUploadResult,
|
|
50
|
+
} from "./components/FileUpload";
|
|
51
|
+
|
|
52
|
+
export { UserProfile } from "./components/UserProfile";
|
|
53
|
+
export type { UserProfileProps } from "./components/UserProfile";
|
|
54
|
+
|
|
55
|
+
export { ForgotPassword, ResetPassword } from "./components/PasswordReset";
|
|
56
|
+
export type {
|
|
57
|
+
ForgotPasswordProps,
|
|
58
|
+
ResetPasswordProps,
|
|
59
|
+
} from "./components/PasswordReset";
|
|
60
|
+
|
|
61
|
+
export { ChatBot } from "./components/ChatBot";
|
|
62
|
+
export type { ChatBotProps, ChatMessage } from "./components/ChatBot";
|
|
63
|
+
|
|
64
|
+
export { EntityList } from "./components/EntityList";
|
|
65
|
+
export type {
|
|
66
|
+
EntityListProps,
|
|
67
|
+
EntityListColumn,
|
|
68
|
+
} from "./components/EntityList";
|
|
69
|
+
|
|
70
|
+
export { EntityForm } from "./components/EntityForm";
|
|
71
|
+
export type {
|
|
72
|
+
EntityFormProps,
|
|
73
|
+
EntityFormField,
|
|
74
|
+
} from "./components/EntityForm";
|
|
75
|
+
|
|
76
|
+
export { Router, Link, Outlet } from "./router/Router";
|
|
77
|
+
export type { RouterProps, LinkProps } from "./router/Router";
|
|
78
|
+
export type { RouteSpec, MatchedRoute } from "./router/match";
|
|
79
|
+
export {
|
|
80
|
+
useRouter,
|
|
81
|
+
useParams,
|
|
82
|
+
useSearchParams,
|
|
83
|
+
usePathname,
|
|
84
|
+
} from "./router/useRouter";
|
|
85
|
+
|
|
86
|
+
export { useAuth } from "./hooks/useAuth";
|
|
87
|
+
export type { UseAuthReturn } from "./hooks/useAuth";
|
|
88
|
+
|
|
89
|
+
// Re-export the lower-level API helpers so apps building custom auth
|
|
90
|
+
// surfaces can drive the same endpoints without duplicating the fetch
|
|
91
|
+
// + error-mapping logic the built-in components use.
|
|
92
|
+
export {
|
|
93
|
+
acceptInvite,
|
|
94
|
+
ApiError,
|
|
95
|
+
changePassword,
|
|
96
|
+
completePasswordReset,
|
|
97
|
+
ensureGuestSession,
|
|
98
|
+
connectionAuthUrl,
|
|
99
|
+
createApiKey,
|
|
100
|
+
createInvite,
|
|
101
|
+
createOrg,
|
|
102
|
+
listActiveSessions,
|
|
103
|
+
listApiKeys,
|
|
104
|
+
listAuthProviders,
|
|
105
|
+
listInvites,
|
|
106
|
+
listOrgMembers,
|
|
107
|
+
listOrgs,
|
|
108
|
+
passwordLogin,
|
|
109
|
+
passwordRegister,
|
|
110
|
+
persistSession,
|
|
111
|
+
removeMember,
|
|
112
|
+
requestPasswordReset,
|
|
113
|
+
revokeAllSessions,
|
|
114
|
+
revokeApiKey,
|
|
115
|
+
revokeInvite,
|
|
116
|
+
sendMagicLink,
|
|
117
|
+
updateMemberRole,
|
|
118
|
+
verifyMagicLink,
|
|
119
|
+
} from "./lib/api";
|
|
120
|
+
export type {
|
|
121
|
+
ActiveSession,
|
|
122
|
+
ApiKeyCreated,
|
|
123
|
+
ApiKeySummary,
|
|
124
|
+
AuthProvider,
|
|
125
|
+
InviteResult,
|
|
126
|
+
OrgMember,
|
|
127
|
+
OrgSummary,
|
|
128
|
+
PendingInvite,
|
|
129
|
+
SessionResponse,
|
|
130
|
+
} from "./lib/api";
|