@robosystems/core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/actions/entity-actions.d.ts +8 -0
- package/actions/entity-actions.js +16 -0
- package/actions/graph-actions.d.ts +3 -0
- package/actions/graph-actions.js +13 -0
- package/auth-components/AppSwitcher.d.ts +6 -0
- package/auth-components/AppSwitcher.js +28 -0
- package/auth-components/AuthGuard.d.ts +7 -0
- package/auth-components/AuthGuard.js +31 -0
- package/auth-components/AuthProvider.d.ts +9 -0
- package/auth-components/AuthProvider.js +565 -0
- package/auth-components/SessionWarningDialog.d.ts +1 -0
- package/auth-components/SessionWarningDialog.js +51 -0
- package/auth-components/SignInForm.d.ts +20 -0
- package/auth-components/SignInForm.js +139 -0
- package/auth-components/SignUpForm.d.ts +15 -0
- package/auth-components/SignUpForm.js +167 -0
- package/auth-components/TurnstileWidget.d.ts +99 -0
- package/auth-components/TurnstileWidget.js +224 -0
- package/auth-components/index.d.ts +6 -0
- package/auth-components/index.js +6 -0
- package/auth-core/cleanup.d.ts +44 -0
- package/auth-core/cleanup.js +157 -0
- package/auth-core/client.d.ts +121 -0
- package/auth-core/client.js +767 -0
- package/auth-core/config.d.ts +19 -0
- package/auth-core/config.js +56 -0
- package/auth-core/hooks.d.ts +19 -0
- package/auth-core/hooks.js +63 -0
- package/auth-core/index.d.ts +6 -0
- package/auth-core/index.js +6 -0
- package/auth-core/sso.d.ts +42 -0
- package/auth-core/sso.js +281 -0
- package/auth-core/token-storage.d.ts +58 -0
- package/auth-core/token-storage.js +177 -0
- package/auth-core/types.d.ts +145 -0
- package/auth-core/types.js +1 -0
- package/components/EntitySelector.d.ts +44 -0
- package/components/EntitySelector.js +64 -0
- package/components/EntitySelectorCore.d.ts +45 -0
- package/components/EntitySelectorCore.js +67 -0
- package/components/GraphSelectorCore.d.ts +21 -0
- package/components/GraphSelectorCore.js +39 -0
- package/components/PageLayout.d.ts +8 -0
- package/components/PageLayout.js +5 -0
- package/components/RepositoryGuard.d.ts +68 -0
- package/components/RepositoryGuard.js +91 -0
- package/components/console/ConsoleContent.d.ts +4 -0
- package/components/console/ConsoleContent.js +612 -0
- package/components/console/ConsoleMarkdown.d.ts +11 -0
- package/components/console/ConsoleMarkdown.js +49 -0
- package/components/console/ProgressiveText.d.ts +6 -0
- package/components/console/ProgressiveText.js +21 -0
- package/components/console/graphAwareConfig.d.ts +70 -0
- package/components/console/graphAwareConfig.js +411 -0
- package/components/console/index.d.ts +6 -0
- package/components/console/index.js +4 -0
- package/components/console/types.d.ts +84 -0
- package/components/console/types.js +1 -0
- package/components/graph-filters.d.ts +75 -0
- package/components/graph-filters.js +111 -0
- package/components/index.d.ts +10 -0
- package/components/index.js +9 -0
- package/components/repositories/ActiveSubscriptions.d.ts +26 -0
- package/components/repositories/ActiveSubscriptions.js +103 -0
- package/components/repositories/BrowseRepositories.d.ts +11 -0
- package/components/repositories/BrowseRepositories.js +184 -0
- package/components/repositories/index.d.ts +2 -0
- package/components/repositories/index.js +2 -0
- package/components/search/SearchContent.d.ts +4 -0
- package/components/search/SearchContent.js +168 -0
- package/components/search/index.d.ts +2 -0
- package/components/search/index.js +1 -0
- package/components/search/types.d.ts +15 -0
- package/components/search/types.js +1 -0
- package/contexts/entity-context.d.ts +28 -0
- package/contexts/entity-context.js +89 -0
- package/contexts/graph-context.d.ts +27 -0
- package/contexts/graph-context.js +128 -0
- package/contexts/index.d.ts +5 -0
- package/contexts/index.js +5 -0
- package/contexts/org-context.d.ts +25 -0
- package/contexts/org-context.js +64 -0
- package/contexts/service-offerings-context.d.ts +45 -0
- package/contexts/service-offerings-context.js +98 -0
- package/contexts/sidebar-context.d.ts +18 -0
- package/contexts/sidebar-context.js +44 -0
- package/hooks/index.d.ts +6 -0
- package/hooks/index.js +6 -0
- package/hooks/use-api-error.d.ts +3 -0
- package/hooks/use-api-error.js +24 -0
- package/hooks/use-media-query.d.ts +1 -0
- package/hooks/use-media-query.js +21 -0
- package/hooks/use-streaming-query.d.ts +22 -0
- package/hooks/use-streaming-query.js +142 -0
- package/hooks/use-toast.d.ts +37 -0
- package/hooks/use-toast.js +117 -0
- package/hooks/use-user-limits.d.ts +12 -0
- package/hooks/use-user-limits.js +73 -0
- package/hooks/use-user.d.ts +11 -0
- package/hooks/use-user.js +39 -0
- package/index.d.ts +29 -0
- package/index.js +77 -0
- package/lib/entity-cookie.d.ts +10 -0
- package/lib/entity-cookie.js +38 -0
- package/lib/graph-cookie.d.ts +11 -0
- package/lib/graph-cookie.js +62 -0
- package/lib/graph-tiers.d.ts +41 -0
- package/lib/graph-tiers.js +107 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/sidebar-cookie.d.ts +11 -0
- package/lib/sidebar-cookie.js +42 -0
- package/library/colors.d.ts +4 -0
- package/library/colors.js +41 -0
- package/library/components/ClassificationPicker.d.ts +8 -0
- package/library/components/ClassificationPicker.js +55 -0
- package/library/components/ElementBrowser.d.ts +11 -0
- package/library/components/ElementBrowser.js +106 -0
- package/library/components/ElementDetail.d.ts +7 -0
- package/library/components/ElementDetail.js +97 -0
- package/library/components/LibraryHierarchy.d.ts +13 -0
- package/library/components/LibraryHierarchy.js +380 -0
- package/library/components/TaxonomySidebar.d.ts +6 -0
- package/library/components/TaxonomySidebar.js +45 -0
- package/library/index.d.ts +8 -0
- package/library/index.js +7 -0
- package/package.json +37 -0
- package/research/CoverageCard.d.ts +12 -0
- package/research/CoverageCard.js +15 -0
- package/research/CoverageGrid.d.ts +6 -0
- package/research/CoverageGrid.js +9 -0
- package/research/CoverageHistory.d.ts +5 -0
- package/research/CoverageHistory.js +7 -0
- package/research/ResearchArticle.d.ts +15 -0
- package/research/ResearchArticle.js +26 -0
- package/research/catalog.d.ts +14 -0
- package/research/catalog.js +42 -0
- package/research/index.d.ts +6 -0
- package/research/index.js +9 -0
- package/research/types.d.ts +38 -0
- package/research/types.js +4 -0
- package/task-monitoring/hooks.d.ts +26 -0
- package/task-monitoring/hooks.js +126 -0
- package/task-monitoring/index.d.ts +5 -0
- package/task-monitoring/index.js +7 -0
- package/task-monitoring/operationErrors.d.ts +82 -0
- package/task-monitoring/operationErrors.js +251 -0
- package/task-monitoring/operationHooks.d.ts +117 -0
- package/task-monitoring/operationHooks.js +536 -0
- package/task-monitoring/operationMonitor.d.ts +78 -0
- package/task-monitoring/operationMonitor.js +200 -0
- package/task-monitoring/operationTypes.d.ts +236 -0
- package/task-monitoring/operationTypes.js +60 -0
- package/task-monitoring/taskMonitor.d.ts +14 -0
- package/task-monitoring/taskMonitor.js +132 -0
- package/task-monitoring/types.d.ts +31 -0
- package/task-monitoring/types.js +1 -0
- package/theme/flowbite-theme.d.ts +758 -0
- package/theme/flowbite-theme.js +765 -0
- package/theme/index.d.ts +1 -0
- package/theme/index.js +1 -0
- package/types/entity.d.ts +19 -0
- package/types/index.d.ts +2 -0
- package/types/index.js +1 -0
- package/types/user.d.ts +6 -0
- package/ui-components/ConfirmModal.d.ts +41 -0
- package/ui-components/ConfirmModal.js +19 -0
- package/ui-components/EmptyState.d.ts +22 -0
- package/ui-components/EmptyState.js +10 -0
- package/ui-components/LoadingState.d.ts +27 -0
- package/ui-components/LoadingState.js +12 -0
- package/ui-components/Logo.d.ts +28 -0
- package/ui-components/Logo.js +95 -0
- package/ui-components/Spinner.d.ts +37 -0
- package/ui-components/Spinner.js +48 -0
- package/ui-components/StatCard.d.ts +21 -0
- package/ui-components/StatCard.js +10 -0
- package/ui-components/api-keys/ApiKeyDisplay.d.ts +13 -0
- package/ui-components/api-keys/ApiKeyDisplay.js +7 -0
- package/ui-components/api-keys/ApiKeyTable.d.ts +9 -0
- package/ui-components/api-keys/ApiKeyTable.js +17 -0
- package/ui-components/api-keys/ApiKeysCard.d.ts +6 -0
- package/ui-components/api-keys/ApiKeysCard.js +97 -0
- package/ui-components/api-keys/CreateApiKeyModal.d.ts +11 -0
- package/ui-components/api-keys/CreateApiKeyModal.js +47 -0
- package/ui-components/api-keys/SecureApiKeyField.d.ts +13 -0
- package/ui-components/api-keys/SecureApiKeyField.js +64 -0
- package/ui-components/api-keys/index.d.ts +5 -0
- package/ui-components/api-keys/index.js +5 -0
- package/ui-components/chat/ChatHeader.d.ts +12 -0
- package/ui-components/chat/ChatHeader.js +5 -0
- package/ui-components/chat/ChatInputArea.d.ts +16 -0
- package/ui-components/chat/ChatInputArea.js +9 -0
- package/ui-components/chat/ChatMessage.d.ts +7 -0
- package/ui-components/chat/ChatMessage.js +24 -0
- package/ui-components/chat/DeepResearchToggle.d.ts +8 -0
- package/ui-components/chat/DeepResearchToggle.js +13 -0
- package/ui-components/chat/index.d.ts +9 -0
- package/ui-components/chat/index.js +8 -0
- package/ui-components/chat/types.d.ts +23 -0
- package/ui-components/chat/types.js +51 -0
- package/ui-components/forms/PasswordRequirements.d.ts +10 -0
- package/ui-components/forms/PasswordRequirements.js +15 -0
- package/ui-components/forms/SettingsCard.d.ts +11 -0
- package/ui-components/forms/SettingsCard.js +5 -0
- package/ui-components/forms/SettingsFormField.d.ts +18 -0
- package/ui-components/forms/SettingsFormField.js +17 -0
- package/ui-components/forms/StatusAlert.d.ts +8 -0
- package/ui-components/forms/StatusAlert.js +7 -0
- package/ui-components/forms/index.d.ts +4 -0
- package/ui-components/forms/index.js +4 -0
- package/ui-components/index.d.ts +13 -0
- package/ui-components/index.js +18 -0
- package/ui-components/layout/CoreNavbar.d.ts +14 -0
- package/ui-components/layout/CoreNavbar.js +53 -0
- package/ui-components/layout/CoreSidebar.d.ts +28 -0
- package/ui-components/layout/CoreSidebar.js +62 -0
- package/ui-components/layout/LandingFooter.d.ts +28 -0
- package/ui-components/layout/LandingFooter.js +34 -0
- package/ui-components/layout/PageContainer.d.ts +8 -0
- package/ui-components/layout/PageContainer.js +17 -0
- package/ui-components/layout/PageHeader.d.ts +23 -0
- package/ui-components/layout/PageHeader.js +12 -0
- package/ui-components/layout/SettingsContainer.d.ts +10 -0
- package/ui-components/layout/SettingsContainer.js +9 -0
- package/ui-components/layout/ThemeToggle.d.ts +4 -0
- package/ui-components/layout/ThemeToggle.js +6 -0
- package/ui-components/layout/index.d.ts +7 -0
- package/ui-components/layout/index.js +7 -0
- package/ui-components/settings/GeneralInformationCard.d.ts +12 -0
- package/ui-components/settings/GeneralInformationCard.js +72 -0
- package/ui-components/settings/PasswordInformationCard.d.ts +10 -0
- package/ui-components/settings/PasswordInformationCard.js +167 -0
- package/ui-components/settings/SettingsPageHeader.d.ts +7 -0
- package/ui-components/settings/SettingsPageHeader.js +6 -0
- package/ui-components/settings/index.d.ts +3 -0
- package/ui-components/settings/index.js +3 -0
- package/ui-components/support/SupportModal.d.ts +15 -0
- package/ui-components/support/SupportModal.js +74 -0
- package/ui-components/support/index.d.ts +2 -0
- package/ui-components/support/index.js +1 -0
- package/ui-components/types/index.d.ts +39 -0
- package/ui-components/types/index.js +1 -0
- package/utils/index.d.ts +31 -0
- package/utils/index.js +38 -0
- package/utils/turnstile-config.d.ts +11 -0
- package/utils/turnstile-config.js +19 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { RoboSystemsAuthClient } from '../auth-core/client';
|
|
5
|
+
import { getAppConfig } from '../auth-core/config';
|
|
6
|
+
import { useSSO } from '../auth-core/sso';
|
|
7
|
+
import { LogoBadge, Spinner } from '../ui-components';
|
|
8
|
+
/**
|
|
9
|
+
* Map a login failure to a user-facing message. A connectivity failure (the
|
|
10
|
+
* request never reached the server — `fetch` throws a `TypeError`) must NOT be
|
|
11
|
+
* reported as bad credentials: that sends users to reset a password that is
|
|
12
|
+
* actually correct. Reached-server auth rejection (401/403, or an empty/invalid
|
|
13
|
+
* auth body) stays "Invalid email or password"; 5xx gets its own message.
|
|
14
|
+
*/
|
|
15
|
+
export function loginErrorMessage(error) {
|
|
16
|
+
var _a, _b, _c;
|
|
17
|
+
const err = error;
|
|
18
|
+
const status = (_a = err === null || err === void 0 ? void 0 : err.status) !== null && _a !== void 0 ? _a : (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status;
|
|
19
|
+
const message = String((_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : '');
|
|
20
|
+
if (error instanceof TypeError ||
|
|
21
|
+
/failed to fetch|networkerror|load failed|fetch failed|err_(connection|network|name_not_resolved)/i.test(message)) {
|
|
22
|
+
return 'Unable to reach the server. Check your connection and try again.';
|
|
23
|
+
}
|
|
24
|
+
if (typeof status === 'number' && status >= 500) {
|
|
25
|
+
return 'The server ran into a problem. Please try again in a moment.';
|
|
26
|
+
}
|
|
27
|
+
return 'Invalid email or password';
|
|
28
|
+
}
|
|
29
|
+
export function SignInForm({ onSuccess, onRedirect, redirectTo = '/home', className = '', apiUrl, enableSSO = true, currentApp, }) {
|
|
30
|
+
const [formData, setFormData] = useState({
|
|
31
|
+
email: '',
|
|
32
|
+
password: '',
|
|
33
|
+
});
|
|
34
|
+
const [loading, setLoading] = useState(false);
|
|
35
|
+
const [error, setError] = useState('');
|
|
36
|
+
const [ssoChecking, setSSOChecking] = useState(enableSSO);
|
|
37
|
+
const [redirecting, setRedirecting] = useState(false);
|
|
38
|
+
const authClient = useMemo(() => new RoboSystemsAuthClient(apiUrl), [apiUrl]);
|
|
39
|
+
const { checkSSOAuthentication, handleSSOLogin } = useSSO(apiUrl);
|
|
40
|
+
// Check for existing authentication or SSO login on mount
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!enableSSO)
|
|
43
|
+
return;
|
|
44
|
+
const checkAuth = async () => {
|
|
45
|
+
try {
|
|
46
|
+
// Capture returnUrl before handleSSOLogin cleans up URL params
|
|
47
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
48
|
+
const ssoReturnUrl = urlParams.get('returnUrl');
|
|
49
|
+
// Check for SSO login from URL parameters
|
|
50
|
+
const ssoUser = await handleSSOLogin();
|
|
51
|
+
if (ssoUser) {
|
|
52
|
+
setRedirecting(true);
|
|
53
|
+
if (onSuccess) {
|
|
54
|
+
onSuccess(ssoUser);
|
|
55
|
+
}
|
|
56
|
+
// handleSSOLogin schedules navigation to returnUrl if present;
|
|
57
|
+
// only redirect to the default if no returnUrl was specified
|
|
58
|
+
if (!ssoReturnUrl) {
|
|
59
|
+
window.location.href = redirectTo;
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Check if user is already authenticated via regular session
|
|
64
|
+
try {
|
|
65
|
+
const existingUser = await checkSSOAuthentication();
|
|
66
|
+
if (existingUser) {
|
|
67
|
+
setRedirecting(true);
|
|
68
|
+
if (onSuccess) {
|
|
69
|
+
onSuccess(existingUser);
|
|
70
|
+
}
|
|
71
|
+
window.location.href = redirectTo;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (_a) {
|
|
76
|
+
// User not authenticated, continue to login form
|
|
77
|
+
}
|
|
78
|
+
// Only hide spinner if authentication failed
|
|
79
|
+
setSSOChecking(false);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (process.env.NODE_ENV === 'development') {
|
|
83
|
+
console.error('Auth check failed:', error);
|
|
84
|
+
}
|
|
85
|
+
setSSOChecking(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
checkAuth();
|
|
89
|
+
}, [
|
|
90
|
+
enableSSO,
|
|
91
|
+
redirectTo,
|
|
92
|
+
onSuccess,
|
|
93
|
+
handleSSOLogin,
|
|
94
|
+
checkSSOAuthentication,
|
|
95
|
+
authClient,
|
|
96
|
+
]);
|
|
97
|
+
const handleInputChange = (e) => {
|
|
98
|
+
const { name, value } = e.target;
|
|
99
|
+
setFormData((prev) => (Object.assign(Object.assign({}, prev), { [name]: value })));
|
|
100
|
+
};
|
|
101
|
+
const handleSubmit = async (e) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
setLoading(true);
|
|
104
|
+
setError('');
|
|
105
|
+
try {
|
|
106
|
+
const result = await authClient.login(formData.email, formData.password);
|
|
107
|
+
// Call onSuccess callback if provided
|
|
108
|
+
if (onSuccess) {
|
|
109
|
+
onSuccess(result.user);
|
|
110
|
+
}
|
|
111
|
+
// Set redirecting state to keep showing loading
|
|
112
|
+
setRedirecting(true);
|
|
113
|
+
// Use window.location.href for reliable redirect
|
|
114
|
+
window.location.href = redirectTo;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
setError(loginErrorMessage(error));
|
|
118
|
+
setLoading(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const handleSignUpClick = () => {
|
|
122
|
+
if (onRedirect) {
|
|
123
|
+
onRedirect('/register');
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
window.location.href = '/register';
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// Determine app name based on currentApp prop
|
|
130
|
+
const appName = getAppConfig(currentApp || 'robosystems').displayName;
|
|
131
|
+
if (ssoChecking || redirecting) {
|
|
132
|
+
return (_jsx("div", { className: "via-primary-950 flex min-h-screen items-center justify-center bg-linear-to-br from-black to-zinc-900 p-6", children: _jsx("div", { className: "w-full max-w-md", children: _jsxs("div", { className: "text-center", children: [_jsx(LogoBadge, { animate: "loop", className: "mx-auto h-16 w-16" }), _jsx("h1", { className: "font-heading mt-4 text-center text-2xl font-semibold tracking-tight text-white", children: appName })] }) }) }));
|
|
133
|
+
}
|
|
134
|
+
return (_jsx("div", { className: "via-primary-950 flex min-h-screen items-center justify-center bg-linear-to-br from-black to-zinc-900 px-4 py-12 sm:px-6 lg:px-8", children: _jsxs("div", { className: "w-full max-w-lg space-y-8", children: [_jsxs("div", { className: "text-center", children: [_jsx(LogoBadge, { animate: "once", className: "mx-auto h-14 w-14" }), _jsx("h1", { className: "font-heading mt-4 text-center text-2xl font-semibold tracking-tight text-white", children: appName }), _jsx("h2", { className: "mt-2 text-center text-xl font-semibold tracking-tight text-gray-300", children: "Sign in to your account" })] }), _jsxs("form", { className: ['mt-8 space-y-6', className].filter(Boolean).join(' '), onSubmit: handleSubmit, children: [error && (_jsx("div", { className: "rounded-md border border-red-800 bg-red-900/50 p-4", children: _jsx("div", { className: "text-sm text-red-300", children: error }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "email", className: "sr-only", children: "Email address" }), _jsx("input", { id: "email", name: "email", type: "email", autoComplete: "email", required: true, value: formData.email, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Email address", disabled: loading || redirecting })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "password", className: "sr-only", children: "Password" }), _jsx("input", { id: "password", name: "password", type: "password", autoComplete: "current-password", required: true, value: formData.password, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Password", disabled: loading || redirecting })] })] }), _jsx("div", { children: _jsxs("button", { type: "submit", disabled: loading || redirecting, className: "group bg-primary-600 hover:bg-primary-700 focus-visible:outline-primary-500 relative flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid disabled:cursor-not-allowed disabled:opacity-50", children: [loading && _jsx(Spinner, { size: "sm", className: "mr-2 text-white" }), redirecting
|
|
135
|
+
? 'Redirecting...'
|
|
136
|
+
: loading
|
|
137
|
+
? 'Signing in...'
|
|
138
|
+
: 'Sign in'] }) }), _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("a", { href: "/auth/forgot-password", className: "hover:text-primary-400 text-sm text-gray-300", children: "Forgot password?" }), _jsx("button", { type: "button", onClick: handleSignUpClick, className: "hover:text-primary-400 text-sm font-medium text-gray-300", children: "Don't have an account? Sign up" })] })] })] }) }));
|
|
139
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AuthUser } from '../auth-core/types';
|
|
3
|
+
export interface SignUpFormProps {
|
|
4
|
+
onSuccess?: (user: AuthUser) => void;
|
|
5
|
+
onRedirect?: (url: string) => void;
|
|
6
|
+
redirectTo?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
showConfirmPassword?: boolean;
|
|
9
|
+
showTermsAcceptance?: boolean;
|
|
10
|
+
autoSignIn?: boolean;
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
currentApp?: string;
|
|
13
|
+
turnstileSiteKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function SignUpForm({ onSuccess, onRedirect, redirectTo, className, showConfirmPassword, showTermsAcceptance, autoSignIn, apiUrl, currentApp, turnstileSiteKey, }: SignUpFormProps): React.JSX.Element;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { RoboSystemsAuthClient } from '../auth-core/client';
|
|
5
|
+
import { getAppConfig } from '../auth-core/config';
|
|
6
|
+
import { LogoBadge, Spinner } from '../ui-components';
|
|
7
|
+
import { TurnstileWidget } from './TurnstileWidget';
|
|
8
|
+
export function SignUpForm({ onSuccess, onRedirect, redirectTo = '/login', className = '', showConfirmPassword = true, showTermsAcceptance = true, autoSignIn = false, apiUrl, currentApp, turnstileSiteKey, }) {
|
|
9
|
+
const [formData, setFormData] = useState({
|
|
10
|
+
name: '',
|
|
11
|
+
email: '',
|
|
12
|
+
password: '',
|
|
13
|
+
confirmPassword: '',
|
|
14
|
+
});
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
18
|
+
const [captchaError, setCaptchaError] = useState(null);
|
|
19
|
+
const [passwordStrength, setPasswordStrength] = useState(null);
|
|
20
|
+
const [checkingPassword, setCheckingPassword] = useState(false);
|
|
21
|
+
const debounceRef = useRef(null);
|
|
22
|
+
const authClientRef = useRef(new RoboSystemsAuthClient(apiUrl));
|
|
23
|
+
const checkPassword = useCallback(async (password, email) => {
|
|
24
|
+
if (password.length < 4) {
|
|
25
|
+
setPasswordStrength(null);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setCheckingPassword(true);
|
|
29
|
+
try {
|
|
30
|
+
const result = await authClientRef.current.checkPasswordStrength(password, email || undefined);
|
|
31
|
+
setPasswordStrength(result);
|
|
32
|
+
}
|
|
33
|
+
catch (_a) {
|
|
34
|
+
setPasswordStrength(null);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setCheckingPassword(false);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (debounceRef.current)
|
|
42
|
+
clearTimeout(debounceRef.current);
|
|
43
|
+
if (formData.password.length < 4) {
|
|
44
|
+
setPasswordStrength(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
debounceRef.current = setTimeout(() => {
|
|
48
|
+
checkPassword(formData.password, formData.email);
|
|
49
|
+
}, 400);
|
|
50
|
+
return () => {
|
|
51
|
+
if (debounceRef.current)
|
|
52
|
+
clearTimeout(debounceRef.current);
|
|
53
|
+
};
|
|
54
|
+
}, [formData.password, formData.email, checkPassword]);
|
|
55
|
+
const handleInputChange = (e) => {
|
|
56
|
+
const { name, value } = e.target;
|
|
57
|
+
setFormData((prev) => (Object.assign(Object.assign({}, prev), { [name]: value })));
|
|
58
|
+
};
|
|
59
|
+
const handleCaptchaVerify = (token) => {
|
|
60
|
+
setCaptchaToken(token);
|
|
61
|
+
setCaptchaError(null);
|
|
62
|
+
};
|
|
63
|
+
const handleCaptchaError = (error) => {
|
|
64
|
+
setCaptchaError(error);
|
|
65
|
+
setCaptchaToken(null);
|
|
66
|
+
};
|
|
67
|
+
const handleCaptchaExpire = () => {
|
|
68
|
+
setCaptchaToken(null);
|
|
69
|
+
setCaptchaError('CAPTCHA expired. Please verify again.');
|
|
70
|
+
};
|
|
71
|
+
const handleSubmit = async (e) => {
|
|
72
|
+
var _a, _b;
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
setLoading(true);
|
|
75
|
+
setError('');
|
|
76
|
+
setCaptchaError(null);
|
|
77
|
+
// Validate confirm password if enabled
|
|
78
|
+
if (showConfirmPassword && formData.password !== formData.confirmPassword) {
|
|
79
|
+
setError('Passwords do not match');
|
|
80
|
+
setLoading(false);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Validate password strength
|
|
84
|
+
if (checkingPassword) {
|
|
85
|
+
setError('Checking password strength, please wait...');
|
|
86
|
+
setLoading(false);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (passwordStrength && !passwordStrength.is_valid) {
|
|
90
|
+
setError(passwordStrength.errors.length > 0
|
|
91
|
+
? passwordStrength.errors.join('. ')
|
|
92
|
+
: 'Password does not meet requirements');
|
|
93
|
+
setLoading(false);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Check CAPTCHA if site key is provided (production mode)
|
|
97
|
+
if (turnstileSiteKey && !captchaToken) {
|
|
98
|
+
setError('Please complete the security verification to continue');
|
|
99
|
+
setLoading(false);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const result = await authClientRef.current.register(formData.email, formData.password, formData.name, captchaToken || undefined);
|
|
104
|
+
// Call onSuccess callback if provided
|
|
105
|
+
if (onSuccess) {
|
|
106
|
+
onSuccess(result.user);
|
|
107
|
+
}
|
|
108
|
+
// Use window.location.href for reliable redirect
|
|
109
|
+
window.location.href = redirectTo;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// Check if it's a CAPTCHA-related error
|
|
113
|
+
if (((_a = error === null || error === void 0 ? void 0 : error.message) === null || _a === void 0 ? void 0 : _a.includes('CAPTCHA')) ||
|
|
114
|
+
((_b = error === null || error === void 0 ? void 0 : error.message) === null || _b === void 0 ? void 0 : _b.includes('captcha'))) {
|
|
115
|
+
setError(error.message || 'CAPTCHA verification failed. Please try again.');
|
|
116
|
+
// Reset CAPTCHA on failure
|
|
117
|
+
setCaptchaToken(null);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
setError((error === null || error === void 0 ? void 0 : error.message) || 'Registration failed. Please try again.');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
setLoading(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const handleSignInClick = () => {
|
|
128
|
+
if (onRedirect) {
|
|
129
|
+
onRedirect('/login');
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
window.location.href = '/login';
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const handleTermsClick = (link) => {
|
|
136
|
+
if (onRedirect) {
|
|
137
|
+
onRedirect(link);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
window.location.href = link;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
// Determine app name based on currentApp prop
|
|
144
|
+
const appName = getAppConfig(currentApp || 'robosystems').displayName;
|
|
145
|
+
return (_jsx("div", { className: "via-primary-950 flex min-h-screen items-center justify-center bg-linear-to-br from-black to-zinc-900 px-4 py-12 sm:px-6 lg:px-8", children: _jsxs("div", { className: "w-full max-w-lg space-y-8", children: [_jsxs("div", { className: "text-center", children: [_jsx(LogoBadge, { animate: "once", className: "mx-auto h-14 w-14" }), _jsx("h1", { className: "font-heading mt-4 text-center text-2xl font-semibold tracking-tight text-white", children: appName }), _jsx("h2", { className: "mt-6 text-center text-xl font-semibold tracking-tight text-gray-300", children: "Create your account" })] }), _jsxs("form", { className: ['mt-8 space-y-6', className].filter(Boolean).join(' '), onSubmit: handleSubmit, children: [error && (_jsx("div", { className: "rounded-md border border-red-800 bg-red-900/50 p-4", children: _jsx("div", { className: "text-sm text-red-300", children: error }) })), captchaError && (_jsx("div", { className: "rounded-md border border-red-800 bg-red-900/50 p-4", children: _jsx("div", { className: "text-sm text-red-300", children: captchaError }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "name", className: "sr-only", children: "Full name" }), _jsx("input", { id: "name", name: "name", type: "text", autoComplete: "name", required: true, value: formData.name, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Full name", disabled: loading })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "email", className: "sr-only", children: "Email address" }), _jsx("input", { id: "email", name: "email", type: "email", autoComplete: "email", required: true, value: formData.email, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Email address", disabled: loading })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "password", className: "sr-only", children: "Password" }), _jsx("input", { id: "password", name: "password", type: "password", autoComplete: "new-password", required: true, value: formData.password, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Password", disabled: loading })] }), showConfirmPassword && (_jsxs("div", { children: [_jsx("label", { htmlFor: "confirmPassword", className: "sr-only", children: "Confirm password" }), _jsx("input", { id: "confirmPassword", name: "confirmPassword", type: "password", autoComplete: "new-password", required: true, value: formData.confirmPassword, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Confirm password", disabled: loading })] })), _jsx("div", { className: "min-h-[2.75rem]", children: passwordStrength && (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-1.5 flex-1 overflow-hidden rounded-full bg-gray-700", children: _jsx("div", { className: [
|
|
146
|
+
'h-full rounded-full transition-all duration-300',
|
|
147
|
+
passwordStrength.score < 30
|
|
148
|
+
? 'bg-red-500'
|
|
149
|
+
: passwordStrength.score < 60
|
|
150
|
+
? 'bg-yellow-500'
|
|
151
|
+
: passwordStrength.score < 80
|
|
152
|
+
? 'bg-primary-400'
|
|
153
|
+
: 'bg-green-500',
|
|
154
|
+
].join(' '), style: { width: `${passwordStrength.score}%` } }) }), _jsx("span", { className: [
|
|
155
|
+
'text-xs font-medium capitalize',
|
|
156
|
+
passwordStrength.score < 30
|
|
157
|
+
? 'text-red-400'
|
|
158
|
+
: passwordStrength.score < 60
|
|
159
|
+
? 'text-yellow-400'
|
|
160
|
+
: passwordStrength.score < 80
|
|
161
|
+
? 'text-primary-300'
|
|
162
|
+
: 'text-green-400',
|
|
163
|
+
].join(' '), children: passwordStrength.strength.replace('-', ' ') })] }), passwordStrength.errors.length > 0 && (_jsx("p", { className: "text-xs text-red-400", children: passwordStrength.errors[0] })), passwordStrength.errors.length === 0 &&
|
|
164
|
+
passwordStrength.suggestions.length > 0 && (_jsx("p", { className: "text-xs text-gray-400", children: passwordStrength.suggestions[0] }))] })) })] }), turnstileSiteKey && (_jsx("div", { className: "flex justify-center", children: _jsx(TurnstileWidget, { siteKey: turnstileSiteKey, onVerify: handleCaptchaVerify, onError: handleCaptchaError, onExpire: handleCaptchaExpire, theme: "dark", className: "flex justify-center", disabled: loading }) })), _jsx("div", { children: _jsxs("button", { type: "submit", disabled: loading ||
|
|
165
|
+
checkingPassword ||
|
|
166
|
+
(turnstileSiteKey && !captchaToken), className: "group bg-primary-600 hover:bg-primary-700 focus-visible:outline-primary-500 relative flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid disabled:cursor-not-allowed disabled:opacity-50", children: [loading && _jsx(Spinner, { size: "sm", className: "mr-2 text-white" }), loading ? 'Creating account...' : 'Create account'] }) }), showTermsAcceptance && (_jsxs("div", { className: "text-center text-xs text-gray-400", children: ["By creating an account, you agree to our", ' ', _jsx("a", { href: `${process.env.NEXT_PUBLIC_ROBOSYSTEMS_APP_URL || 'https://robosystems.ai'}/pages/terms`, target: "_blank", rel: "noopener noreferrer", className: "hover:text-primary-400 font-medium text-gray-300", children: "Terms of Service" }), ' ', "and", ' ', _jsx("a", { href: `${process.env.NEXT_PUBLIC_ROBOSYSTEMS_APP_URL || 'https://robosystems.ai'}/pages/privacy`, target: "_blank", rel: "noopener noreferrer", className: "hover:text-primary-400 font-medium text-gray-300", children: "Privacy Policy" })] })), _jsx("div", { className: "text-center", children: _jsx("button", { type: "button", onClick: handleSignInClick, className: "hover:text-primary-400 font-medium text-gray-300", children: "Already have an account? Sign in" }) })] })] }) }));
|
|
167
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface TurnstileWidgetProps {
|
|
3
|
+
/** Cloudflare Turnstile site key obtained from dashboard */
|
|
4
|
+
siteKey: string;
|
|
5
|
+
/** Callback fired when CAPTCHA verification succeeds */
|
|
6
|
+
onVerify: (token: string) => void;
|
|
7
|
+
/** Callback fired when CAPTCHA verification fails */
|
|
8
|
+
onError?: (error: string) => void;
|
|
9
|
+
/** Callback fired when CAPTCHA token expires */
|
|
10
|
+
onExpire?: () => void;
|
|
11
|
+
/** Callback fired when Turnstile script loads successfully */
|
|
12
|
+
onLoad?: () => void;
|
|
13
|
+
/** Widget theme - matches your site's color scheme */
|
|
14
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
15
|
+
/** Widget size - use 'compact' for mobile-friendly layouts */
|
|
16
|
+
size?: 'normal' | 'compact';
|
|
17
|
+
/** Language code for widget text (e.g., 'en', 'es', 'fr') */
|
|
18
|
+
language?: string;
|
|
19
|
+
/** Additional CSS classes for styling the widget container */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Whether the widget should be disabled (prevents interaction) */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface TurnstileWidgetRef {
|
|
25
|
+
/** Reset the widget to its initial state, clearing any verification */
|
|
26
|
+
reset: () => void;
|
|
27
|
+
/** Get the current verification token if available */
|
|
28
|
+
getResponse: () => string | undefined;
|
|
29
|
+
}
|
|
30
|
+
declare global {
|
|
31
|
+
interface Window {
|
|
32
|
+
turnstile?: {
|
|
33
|
+
render: (element: HTMLElement | string, options: {
|
|
34
|
+
sitekey: string;
|
|
35
|
+
callback?: (token: string) => void;
|
|
36
|
+
'error-callback'?: (error: string) => void;
|
|
37
|
+
'expired-callback'?: () => void;
|
|
38
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
39
|
+
size?: 'normal' | 'compact';
|
|
40
|
+
language?: string;
|
|
41
|
+
}) => string;
|
|
42
|
+
reset: (widgetId?: string) => void;
|
|
43
|
+
remove: (widgetId?: string) => void;
|
|
44
|
+
getResponse: (widgetId?: string) => string | undefined;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* TurnstileWidget - A React component for Cloudflare Turnstile CAPTCHA
|
|
50
|
+
*
|
|
51
|
+
* This component provides a secure, privacy-focused CAPTCHA solution that:
|
|
52
|
+
* - Automatically loads the Turnstile script when needed
|
|
53
|
+
* - Handles widget lifecycle (render, reset, cleanup)
|
|
54
|
+
* - Provides TypeScript support with comprehensive types
|
|
55
|
+
* - Supports themes, languages, and different sizes
|
|
56
|
+
* - Prevents memory leaks with proper cleanup
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* <TurnstileWidget
|
|
61
|
+
* siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
|
|
62
|
+
* onVerify={(token) => console.log('Verified:', token)}
|
|
63
|
+
* theme="dark"
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare const TurnstileWidget: React.ForwardRefExoticComponent<TurnstileWidgetProps & React.RefAttributes<TurnstileWidgetRef>>;
|
|
68
|
+
/**
|
|
69
|
+
* useTurnstile - React hook for simplified Turnstile integration
|
|
70
|
+
*
|
|
71
|
+
* This hook manages the Turnstile widget state and provides:
|
|
72
|
+
* - Automatic token management
|
|
73
|
+
* - Error and expiration handling
|
|
74
|
+
* - Loading state tracking
|
|
75
|
+
* - Pre-configured TurnstileWidget component
|
|
76
|
+
*
|
|
77
|
+
* @param siteKey - Your Cloudflare Turnstile site key
|
|
78
|
+
* @returns Object containing token, error, loading state, and configured widget
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* const { token, error, isLoading, TurnstileWidget } = useTurnstile(siteKey)
|
|
83
|
+
*
|
|
84
|
+
* return (
|
|
85
|
+
* <form onSubmit={handleSubmit}>
|
|
86
|
+
* <TurnstileWidget theme="dark" />
|
|
87
|
+
* <button disabled={!token}>Submit</button>
|
|
88
|
+
* </form>
|
|
89
|
+
* )
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function useTurnstile(siteKey: string): {
|
|
93
|
+
token: string;
|
|
94
|
+
error: string;
|
|
95
|
+
isLoading: boolean;
|
|
96
|
+
isExpired: boolean;
|
|
97
|
+
reset: () => void;
|
|
98
|
+
TurnstileWidget: (props: Omit<TurnstileWidgetProps, "siteKey" | "onVerify" | "onError" | "onExpire" | "onLoad">) => React.JSX.Element;
|
|
99
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* TurnstileWidget - A React component for Cloudflare Turnstile CAPTCHA
|
|
6
|
+
*
|
|
7
|
+
* This component provides a secure, privacy-focused CAPTCHA solution that:
|
|
8
|
+
* - Automatically loads the Turnstile script when needed
|
|
9
|
+
* - Handles widget lifecycle (render, reset, cleanup)
|
|
10
|
+
* - Provides TypeScript support with comprehensive types
|
|
11
|
+
* - Supports themes, languages, and different sizes
|
|
12
|
+
* - Prevents memory leaks with proper cleanup
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <TurnstileWidget
|
|
17
|
+
* siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
|
|
18
|
+
* onVerify={(token) => console.log('Verified:', token)}
|
|
19
|
+
* theme="dark"
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const TurnstileWidget = React.forwardRef(({ siteKey, onVerify, onError, onExpire, onLoad, theme = 'auto', size = 'normal', language, className = '', disabled = false, }, ref) => {
|
|
24
|
+
const containerRef = useRef(null);
|
|
25
|
+
const widgetIdRef = useRef(null);
|
|
26
|
+
const scriptLoadedRef = useRef(false);
|
|
27
|
+
const loadTurnstileScript = useCallback(() => {
|
|
28
|
+
if (scriptLoadedRef.current ||
|
|
29
|
+
document.querySelector('script[src*="challenges.cloudflare.com"]')) {
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const script = document.createElement('script');
|
|
34
|
+
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
|
|
35
|
+
script.async = true;
|
|
36
|
+
script.defer = true;
|
|
37
|
+
script.onload = () => {
|
|
38
|
+
scriptLoadedRef.current = true;
|
|
39
|
+
onLoad === null || onLoad === void 0 ? void 0 : onLoad();
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
script.onerror = () => {
|
|
43
|
+
reject(new Error('Failed to load Turnstile script'));
|
|
44
|
+
};
|
|
45
|
+
document.head.appendChild(script);
|
|
46
|
+
});
|
|
47
|
+
}, [onLoad]);
|
|
48
|
+
const renderWidget = useCallback(() => {
|
|
49
|
+
if (!containerRef.current || !window.turnstile || !siteKey || disabled) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Remove existing widget if present
|
|
53
|
+
if (widgetIdRef.current) {
|
|
54
|
+
try {
|
|
55
|
+
window.turnstile.remove(widgetIdRef.current);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn('Failed to remove existing Turnstile widget:', error);
|
|
59
|
+
}
|
|
60
|
+
widgetIdRef.current = null;
|
|
61
|
+
}
|
|
62
|
+
// Clear container
|
|
63
|
+
containerRef.current.innerHTML = '';
|
|
64
|
+
try {
|
|
65
|
+
widgetIdRef.current = window.turnstile.render(containerRef.current, {
|
|
66
|
+
sitekey: siteKey,
|
|
67
|
+
callback: (token) => {
|
|
68
|
+
onVerify(token);
|
|
69
|
+
},
|
|
70
|
+
'error-callback': (error) => {
|
|
71
|
+
onError === null || onError === void 0 ? void 0 : onError(error);
|
|
72
|
+
},
|
|
73
|
+
'expired-callback': () => {
|
|
74
|
+
onExpire === null || onExpire === void 0 ? void 0 : onExpire();
|
|
75
|
+
},
|
|
76
|
+
theme: theme,
|
|
77
|
+
size: size,
|
|
78
|
+
language,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error('Failed to render Turnstile widget:', error);
|
|
83
|
+
onError === null || onError === void 0 ? void 0 : onError('Failed to render CAPTCHA widget');
|
|
84
|
+
}
|
|
85
|
+
}, [siteKey, onVerify, onError, onExpire, theme, size, language, disabled]);
|
|
86
|
+
const reset = useCallback(() => {
|
|
87
|
+
if (widgetIdRef.current && window.turnstile) {
|
|
88
|
+
try {
|
|
89
|
+
window.turnstile.reset(widgetIdRef.current);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.warn('Failed to reset Turnstile widget:', error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
const getResponse = useCallback(() => {
|
|
97
|
+
if (widgetIdRef.current && window.turnstile) {
|
|
98
|
+
try {
|
|
99
|
+
return window.turnstile.getResponse(widgetIdRef.current);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.warn('Failed to get Turnstile response:', error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}, []);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!siteKey || disabled) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const initWidget = async () => {
|
|
112
|
+
try {
|
|
113
|
+
await loadTurnstileScript();
|
|
114
|
+
// Wait a bit for the script to fully initialize
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
if (window.turnstile) {
|
|
117
|
+
renderWidget();
|
|
118
|
+
}
|
|
119
|
+
}, 100);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error('Failed to initialize Turnstile:', error);
|
|
123
|
+
onError === null || onError === void 0 ? void 0 : onError('Failed to load CAPTCHA');
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
initWidget();
|
|
127
|
+
return () => {
|
|
128
|
+
if (widgetIdRef.current && window.turnstile) {
|
|
129
|
+
try {
|
|
130
|
+
window.turnstile.remove(widgetIdRef.current);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.warn('Failed to cleanup Turnstile widget:', error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
widgetIdRef.current = null;
|
|
137
|
+
};
|
|
138
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
139
|
+
}, [siteKey, disabled]);
|
|
140
|
+
// Re-render widget ONLY when theme, size, or language actually changes
|
|
141
|
+
// Remove renderWidget from dependencies to prevent infinite loop
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (widgetIdRef.current && window.turnstile && !disabled) {
|
|
144
|
+
renderWidget();
|
|
145
|
+
}
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
}, [theme, size, language, disabled]);
|
|
148
|
+
// Expose reset and getResponse methods via ref
|
|
149
|
+
React.useImperativeHandle(ref, () => ({
|
|
150
|
+
reset,
|
|
151
|
+
getResponse,
|
|
152
|
+
}), [reset, getResponse]);
|
|
153
|
+
if (!siteKey || disabled) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return (_jsx("div", { className: className, children: _jsx("div", { ref: containerRef, className: "flex justify-center", "data-testid": "turnstile-widget" }) }));
|
|
157
|
+
});
|
|
158
|
+
TurnstileWidget.displayName = 'TurnstileWidget';
|
|
159
|
+
/**
|
|
160
|
+
* useTurnstile - React hook for simplified Turnstile integration
|
|
161
|
+
*
|
|
162
|
+
* This hook manages the Turnstile widget state and provides:
|
|
163
|
+
* - Automatic token management
|
|
164
|
+
* - Error and expiration handling
|
|
165
|
+
* - Loading state tracking
|
|
166
|
+
* - Pre-configured TurnstileWidget component
|
|
167
|
+
*
|
|
168
|
+
* @param siteKey - Your Cloudflare Turnstile site key
|
|
169
|
+
* @returns Object containing token, error, loading state, and configured widget
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```tsx
|
|
173
|
+
* const { token, error, isLoading, TurnstileWidget } = useTurnstile(siteKey)
|
|
174
|
+
*
|
|
175
|
+
* return (
|
|
176
|
+
* <form onSubmit={handleSubmit}>
|
|
177
|
+
* <TurnstileWidget theme="dark" />
|
|
178
|
+
* <button disabled={!token}>Submit</button>
|
|
179
|
+
* </form>
|
|
180
|
+
* )
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function useTurnstile(siteKey) {
|
|
184
|
+
const [token, setToken] = React.useState(null);
|
|
185
|
+
const [error, setError] = React.useState(null);
|
|
186
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
187
|
+
const [isExpired, setIsExpired] = React.useState(false);
|
|
188
|
+
const handleVerify = useCallback((verifiedToken) => {
|
|
189
|
+
setToken(verifiedToken);
|
|
190
|
+
setError(null);
|
|
191
|
+
setIsExpired(false);
|
|
192
|
+
setIsLoading(false);
|
|
193
|
+
}, []);
|
|
194
|
+
const handleError = useCallback((errorMessage) => {
|
|
195
|
+
setError(errorMessage);
|
|
196
|
+
setToken(null);
|
|
197
|
+
setIsExpired(false);
|
|
198
|
+
setIsLoading(false);
|
|
199
|
+
}, []);
|
|
200
|
+
const handleExpire = useCallback(() => {
|
|
201
|
+
setIsExpired(true);
|
|
202
|
+
setToken(null);
|
|
203
|
+
setError(null);
|
|
204
|
+
setIsLoading(false);
|
|
205
|
+
}, []);
|
|
206
|
+
const handleLoad = useCallback(() => {
|
|
207
|
+
setIsLoading(false);
|
|
208
|
+
setError(null);
|
|
209
|
+
}, []);
|
|
210
|
+
const reset = useCallback(() => {
|
|
211
|
+
setToken(null);
|
|
212
|
+
setError(null);
|
|
213
|
+
setIsExpired(false);
|
|
214
|
+
setIsLoading(true);
|
|
215
|
+
}, []);
|
|
216
|
+
return {
|
|
217
|
+
token,
|
|
218
|
+
error,
|
|
219
|
+
isLoading,
|
|
220
|
+
isExpired,
|
|
221
|
+
reset,
|
|
222
|
+
TurnstileWidget: React.useCallback((props) => (_jsx(TurnstileWidget, Object.assign({ siteKey: siteKey, onVerify: handleVerify, onError: handleError, onExpire: handleExpire, onLoad: handleLoad }, props))), [siteKey, handleVerify, handleError, handleExpire, handleLoad]),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AppSwitcher } from './AppSwitcher';
|
|
2
|
+
export { AuthGuard } from './AuthGuard';
|
|
3
|
+
export { AuthProvider, useAuth } from './AuthProvider';
|
|
4
|
+
export { SignInForm } from './SignInForm';
|
|
5
|
+
export { SignUpForm } from './SignUpForm';
|
|
6
|
+
export { TurnstileWidget, useTurnstile } from './TurnstileWidget';
|