@robosystems/core 0.6.1 → 0.6.3
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/auth-components/AuthProvider.d.ts +7 -0
- package/auth-components/AuthProvider.js +9 -0
- package/auth-components/SignInForm.d.ts +6 -0
- package/auth-components/SignInForm.js +22 -1
- package/auth-components/index.d.ts +1 -1
- package/auth-components/index.js +1 -1
- package/auth-core/types.d.ts +1 -1
- package/package.json +1 -1
- package/ui-components/api-keys/ApiKeysCard.d.ts +5 -0
- package/ui-components/api-keys/ApiKeysCard.js +20 -5
- package/ui-components/api-keys/CreateApiKeyModal.d.ts +7 -0
- package/ui-components/api-keys/CreateApiKeyModal.js +9 -3
- package/ui-components/settings/PasswordInformationCard.js +36 -10
|
@@ -6,4 +6,11 @@ interface AuthProviderProps {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function AuthProvider({ children, apiUrl, }: AuthProviderProps): import("react").JSX.Element;
|
|
8
8
|
export declare function useAuth(): AuthContextType;
|
|
9
|
+
/**
|
|
10
|
+
* Like `useAuth`, but returns `null` outside an `AuthProvider` instead of
|
|
11
|
+
* throwing. For components that integrate with auth when it is present but must
|
|
12
|
+
* still render without it — design-system previews and isolated component tests
|
|
13
|
+
* mount components with no provider around them.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useOptionalAuth(): AuthContextType | null;
|
|
9
16
|
export {};
|
|
@@ -562,3 +562,12 @@ export function useAuth() {
|
|
|
562
562
|
}
|
|
563
563
|
return context;
|
|
564
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Like `useAuth`, but returns `null` outside an `AuthProvider` instead of
|
|
567
|
+
* throwing. For components that integrate with auth when it is present but must
|
|
568
|
+
* still render without it — design-system previews and isolated component tests
|
|
569
|
+
* mount components with no provider around them.
|
|
570
|
+
*/
|
|
571
|
+
export function useOptionalAuth() {
|
|
572
|
+
return useContext(AuthContext);
|
|
573
|
+
}
|
|
@@ -17,4 +17,10 @@ export interface SignInFormProps {
|
|
|
17
17
|
* auth body) stays "Invalid email or password"; 5xx gets its own message.
|
|
18
18
|
*/
|
|
19
19
|
export declare function loginErrorMessage(error: unknown): string;
|
|
20
|
+
/**
|
|
21
|
+
* Messages for the `?reason=` a redirect into the login page can carry. The
|
|
22
|
+
* producer is `AuthProvider`'s `logout(reason)`, which sends the user to
|
|
23
|
+
* `/login?reason=<reason>`; without this the redirect arrives unexplained.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SIGN_IN_NOTICES: Record<string, string>;
|
|
20
26
|
export declare function SignInForm({ onSuccess, onRedirect, redirectTo, className, apiUrl, enableSSO, currentApp, }: SignInFormProps): React.JSX.Element;
|
|
@@ -26,6 +26,16 @@ export function loginErrorMessage(error) {
|
|
|
26
26
|
}
|
|
27
27
|
return 'Invalid email or password';
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Messages for the `?reason=` a redirect into the login page can carry. The
|
|
31
|
+
* producer is `AuthProvider`'s `logout(reason)`, which sends the user to
|
|
32
|
+
* `/login?reason=<reason>`; without this the redirect arrives unexplained.
|
|
33
|
+
*/
|
|
34
|
+
export const SIGN_IN_NOTICES = {
|
|
35
|
+
password_changed: 'Your password was updated. Please sign in with your new password.',
|
|
36
|
+
session_expired: 'Your session expired. Please sign in again.',
|
|
37
|
+
session_invalid: 'Your session is no longer valid. Please sign in again.',
|
|
38
|
+
};
|
|
29
39
|
export function SignInForm({ onSuccess, onRedirect, redirectTo = '/home', className = '', apiUrl, enableSSO = true, currentApp, }) {
|
|
30
40
|
const [formData, setFormData] = useState({
|
|
31
41
|
email: '',
|
|
@@ -35,6 +45,17 @@ export function SignInForm({ onSuccess, onRedirect, redirectTo = '/home', classN
|
|
|
35
45
|
const [error, setError] = useState('');
|
|
36
46
|
const [ssoChecking, setSSOChecking] = useState(enableSSO);
|
|
37
47
|
const [redirecting, setRedirecting] = useState(false);
|
|
48
|
+
const [notice, setNotice] = useState('');
|
|
49
|
+
// Read via window.location rather than useSearchParams(): the latter forces
|
|
50
|
+
// the consuming app to wrap this component in a Suspense boundary during
|
|
51
|
+
// static prerender, and the login pages render it bare. Runs before the SSO
|
|
52
|
+
// effect below, which strips its own params from the URL.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const reason = new URLSearchParams(window.location.search).get('reason');
|
|
55
|
+
if (reason && SIGN_IN_NOTICES[reason]) {
|
|
56
|
+
setNotice(SIGN_IN_NOTICES[reason]);
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
38
59
|
const authClient = useMemo(() => new RoboSystemsAuthClient(apiUrl), [apiUrl]);
|
|
39
60
|
const { checkSSOAuthentication, handleSSOLogin } = useSSO(apiUrl);
|
|
40
61
|
// Check for existing authentication or SSO login on mount
|
|
@@ -131,7 +152,7 @@ export function SignInForm({ onSuccess, onRedirect, redirectTo = '/home', classN
|
|
|
131
152
|
if (ssoChecking || redirecting) {
|
|
132
153
|
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
154
|
}
|
|
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
|
|
155
|
+
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 }) })), notice && !error && (_jsx("div", { className: "border-primary-800 bg-primary-900/50 rounded-md border p-4", children: _jsx("div", { className: "text-primary-200 text-sm", children: notice }) })), _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
156
|
? 'Redirecting...'
|
|
136
157
|
: loading
|
|
137
158
|
? 'Signing in...'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { AppSwitcher } from './AppSwitcher';
|
|
2
2
|
export { AuthGuard } from './AuthGuard';
|
|
3
|
-
export { AuthProvider, useAuth } from './AuthProvider';
|
|
3
|
+
export { AuthProvider, useAuth, useOptionalAuth } from './AuthProvider';
|
|
4
4
|
export { SignInForm } from './SignInForm';
|
|
5
5
|
export { SignUpForm } from './SignUpForm';
|
|
6
6
|
export { TurnstileWidget, useTurnstile } from './TurnstileWidget';
|
package/auth-components/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { AppSwitcher } from './AppSwitcher';
|
|
2
2
|
export { AuthGuard } from './AuthGuard';
|
|
3
|
-
export { AuthProvider, useAuth } from './AuthProvider';
|
|
3
|
+
export { AuthProvider, useAuth, useOptionalAuth } from './AuthProvider';
|
|
4
4
|
export { SignInForm } from './SignInForm';
|
|
5
5
|
export { SignUpForm } from './SignUpForm';
|
|
6
6
|
export { TurnstileWidget, useTurnstile } from './TurnstileWidget';
|
package/auth-core/types.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface AuthContextType {
|
|
|
71
71
|
};
|
|
72
72
|
login: (email: string, password: string) => Promise<AuthUser>;
|
|
73
73
|
register: (email: string, password: string, name?: string) => Promise<AuthUser>;
|
|
74
|
-
logout: () => Promise<void>;
|
|
74
|
+
logout: (reason?: string) => Promise<void>;
|
|
75
75
|
refreshUser: () => Promise<AuthUser | null>;
|
|
76
76
|
refreshSession: (force?: boolean) => Promise<void>;
|
|
77
77
|
forgotPassword: (email: string) => Promise<{
|
package/package.json
CHANGED
|
@@ -2,5 +2,10 @@ import React from 'react';
|
|
|
2
2
|
export interface ApiKeysCardProps {
|
|
3
3
|
theme?: any;
|
|
4
4
|
className?: string;
|
|
5
|
+
/**
|
|
6
|
+
* When set, the card links MCP users to the app's Connect page (the
|
|
7
|
+
* ready-made connector-URL flow) — e.g. "/connect" in robosystems-app.
|
|
8
|
+
*/
|
|
9
|
+
connectHref?: string;
|
|
5
10
|
}
|
|
6
11
|
export declare const ApiKeysCard: React.FC<ApiKeysCardProps>;
|
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as SDK from '@robosystems/client';
|
|
4
4
|
import { Button } from 'flowbite-react';
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
5
|
+
import { useContext, useEffect, useState } from 'react';
|
|
6
6
|
import { HiKey, HiPlusCircle } from 'react-icons/hi';
|
|
7
|
+
import { GraphContext } from '../../contexts';
|
|
7
8
|
import { Spinner } from '../Spinner';
|
|
8
9
|
import { SettingsCard } from '../forms/SettingsCard';
|
|
9
10
|
import { StatusAlert } from '../forms/StatusAlert';
|
|
10
11
|
import { ApiKeyTable } from './ApiKeyTable';
|
|
11
12
|
import { CreateApiKeyModal } from './CreateApiKeyModal';
|
|
12
|
-
export const ApiKeysCard = ({ theme, className = '', }) => {
|
|
13
|
+
export const ApiKeysCard = ({ theme, className = '', connectHref, }) => {
|
|
14
|
+
var _a;
|
|
15
|
+
// Read the graph context leniently: outside a GraphProvider the scope
|
|
16
|
+
// selector simply doesn't render, preserving the card's old standalone
|
|
17
|
+
// contract instead of throwing.
|
|
18
|
+
const graphContext = useContext(GraphContext);
|
|
19
|
+
const availableGraphs = (_a = graphContext === null || graphContext === void 0 ? void 0 : graphContext.state.graphs) !== null && _a !== void 0 ? _a : [];
|
|
13
20
|
const [apiKeys, setApiKeys] = useState([]);
|
|
14
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
15
22
|
const [isOpenCreateModal, setIsOpenCreateModal] = useState(false);
|
|
@@ -49,12 +56,13 @@ export const ApiKeysCard = ({ theme, className = '', }) => {
|
|
|
49
56
|
}
|
|
50
57
|
};
|
|
51
58
|
const handleCreateKey = async (data) => {
|
|
52
|
-
var _a;
|
|
59
|
+
var _a, _b;
|
|
53
60
|
// Always use SDK to create key
|
|
54
61
|
const response = await SDK.createUserApiKey({
|
|
55
62
|
body: {
|
|
56
63
|
name: data.name,
|
|
57
64
|
description: `API key for ${data.name}`,
|
|
65
|
+
graph_id: data.graphId,
|
|
58
66
|
},
|
|
59
67
|
});
|
|
60
68
|
await fetchApiKeys(); // Refresh the list
|
|
@@ -64,11 +72,12 @@ export const ApiKeysCard = ({ theme, className = '', }) => {
|
|
|
64
72
|
return {
|
|
65
73
|
id: (apiKeyInfo === null || apiKeyInfo === void 0 ? void 0 : apiKeyInfo.id) || '',
|
|
66
74
|
name: (apiKeyInfo === null || apiKeyInfo === void 0 ? void 0 : apiKeyInfo.name) || data.name,
|
|
75
|
+
graphId: (_a = apiKeyInfo === null || apiKeyInfo === void 0 ? void 0 : apiKeyInfo.graph_id) !== null && _a !== void 0 ? _a : data.graphId,
|
|
67
76
|
key: (responseData === null || responseData === void 0 ? void 0 : responseData.key) || '', // The actual API key value
|
|
68
77
|
createdAt: (apiKeyInfo === null || apiKeyInfo === void 0 ? void 0 : apiKeyInfo.created_at) || new Date().toISOString(),
|
|
69
78
|
lastUsedAt: (apiKeyInfo === null || apiKeyInfo === void 0 ? void 0 : apiKeyInfo.last_used_at) || null,
|
|
70
79
|
expiresAt: null,
|
|
71
|
-
isActive: (
|
|
80
|
+
isActive: (_b = apiKeyInfo === null || apiKeyInfo === void 0 ? void 0 : apiKeyInfo.is_active) !== null && _b !== void 0 ? _b : true,
|
|
72
81
|
isSystem: false,
|
|
73
82
|
};
|
|
74
83
|
};
|
|
@@ -93,5 +102,11 @@ export const ApiKeysCard = ({ theme, className = '', }) => {
|
|
|
93
102
|
day: 'numeric',
|
|
94
103
|
});
|
|
95
104
|
};
|
|
96
|
-
return (_jsxs(SettingsCard, { title: "API Keys", description: "Manage your API keys for programmatic access", icon: HiKey, theme: theme === null || theme === void 0 ? void 0 : theme.card, className: className, children: [_jsx("div", { className: "mb-4 flex items-center justify-end", children: _jsxs(Button, { theme: theme === null || theme === void 0 ? void 0 : theme.button, color: "blue", onClick: () => setIsOpenCreateModal(true), children: [_jsx(HiPlusCircle, { className: "mr-2 h-5 w-5" }), "Create API Key"] }) }), error && (_jsx(StatusAlert, { type: "error", message: error, theme: theme === null || theme === void 0 ? void 0 : theme.alert })), isLoading ? (_jsx("div", { className: "flex justify-center py-4", children: _jsx(Spinner, { size: "lg" }) })) : !Array.isArray(apiKeys) || apiKeys.length === 0 ? (_jsxs("div", { className: "rounded-lg border border-zinc-200 bg-zinc-50 p-8 text-center dark:border-zinc-700 dark:bg-zinc-800", children: [_jsx(HiKey, { className: "mx-auto mb-3 h-12 w-12 text-zinc-400" }), _jsx("h3", { className: "mb-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: "No API keys yet" }), _jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Create an API key to access your graphs programmatically" })] })) : (_jsx(ApiKeyTable, { apiKeys: apiKeys, onRevokeKey: handleRevokeKey, theme: theme, formatDate: formatDate })), _jsx(CreateApiKeyModal, { isOpen: isOpenCreateModal, onClose: () => setIsOpenCreateModal(false), onCreateKey: handleCreateKey, theme: theme
|
|
105
|
+
return (_jsxs(SettingsCard, { title: "API Keys", description: "Manage your API keys for programmatic access", icon: HiKey, theme: theme === null || theme === void 0 ? void 0 : theme.card, className: className, children: [_jsx("div", { className: "mb-4 flex items-center justify-end", children: _jsxs(Button, { theme: theme === null || theme === void 0 ? void 0 : theme.button, color: "blue", onClick: () => setIsOpenCreateModal(true), children: [_jsx(HiPlusCircle, { className: "mr-2 h-5 w-5" }), "Create API Key"] }) }), error && (_jsx(StatusAlert, { type: "error", message: error, theme: theme === null || theme === void 0 ? void 0 : theme.alert })), isLoading ? (_jsx("div", { className: "flex justify-center py-4", children: _jsx(Spinner, { size: "lg" }) })) : !Array.isArray(apiKeys) || apiKeys.length === 0 ? (_jsxs("div", { className: "rounded-lg border border-zinc-200 bg-zinc-50 p-8 text-center dark:border-zinc-700 dark:bg-zinc-800", children: [_jsx(HiKey, { className: "mx-auto mb-3 h-12 w-12 text-zinc-400" }), _jsx("h3", { className: "mb-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: "No API keys yet" }), _jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Create an API key to access your graphs programmatically" })] })) : (_jsx(ApiKeyTable, { apiKeys: apiKeys, onRevokeKey: handleRevokeKey, theme: theme, formatDate: formatDate })), connectHref && (_jsxs("p", { className: "mt-4 text-sm text-zinc-500 dark:text-zinc-400", children: ["Connecting Claude or another MCP client? Generate a ready-made connector URL from the", ' ', _jsx("a", { href: connectHref, className: "text-primary-600 dark:text-primary-400 font-medium hover:underline", children: "Connect page" }), "."] })), _jsx(CreateApiKeyModal, { isOpen: isOpenCreateModal, onClose: () => setIsOpenCreateModal(false), onCreateKey: handleCreateKey, theme: theme, graphs: availableGraphs.map((g) => {
|
|
106
|
+
var _a;
|
|
107
|
+
return ({
|
|
108
|
+
graphId: g.graphId,
|
|
109
|
+
graphName: (_a = g.graphName) !== null && _a !== void 0 ? _a : g.graphId,
|
|
110
|
+
});
|
|
111
|
+
}) })] }));
|
|
97
112
|
};
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ApiKeyWithValue } from '../types';
|
|
3
|
+
export interface CreateApiKeyModalGraph {
|
|
4
|
+
graphId: string;
|
|
5
|
+
graphName: string;
|
|
6
|
+
}
|
|
3
7
|
export interface CreateApiKeyModalProps {
|
|
4
8
|
isOpen: boolean;
|
|
5
9
|
onClose: () => void;
|
|
6
10
|
onCreateKey: (data: {
|
|
7
11
|
name: string;
|
|
12
|
+
graphId?: string;
|
|
8
13
|
}) => Promise<ApiKeyWithValue>;
|
|
9
14
|
theme?: any;
|
|
15
|
+
/** Graphs offered as scope choices; omit to create account-wide keys only */
|
|
16
|
+
graphs?: CreateApiKeyModalGraph[];
|
|
10
17
|
}
|
|
11
18
|
export declare const CreateApiKeyModal: React.FC<CreateApiKeyModalProps>;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { Button, Label, Modal, ModalBody, ModalFooter, ModalHeader, } from 'flowbite-react';
|
|
3
|
+
import { Button, Label, Modal, ModalBody, ModalFooter, ModalHeader, Select, } from 'flowbite-react';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { Spinner } from '../Spinner';
|
|
6
6
|
import { SettingsFormField } from '../forms/SettingsFormField';
|
|
7
7
|
import { StatusAlert } from '../forms/StatusAlert';
|
|
8
8
|
import { ApiKeyDisplay } from './ApiKeyDisplay';
|
|
9
|
-
export const CreateApiKeyModal = ({ isOpen, onClose, onCreateKey, theme, }) => {
|
|
9
|
+
export const CreateApiKeyModal = ({ isOpen, onClose, onCreateKey, theme, graphs, }) => {
|
|
10
|
+
var _a;
|
|
10
11
|
const [isCreating, setIsCreating] = useState(false);
|
|
11
12
|
const [newApiKey, setNewApiKey] = useState(null);
|
|
12
13
|
const [keyName, setKeyName] = useState('');
|
|
14
|
+
const [scopeGraphId, setScopeGraphId] = useState('');
|
|
13
15
|
const [error, setError] = useState(null);
|
|
14
16
|
const handleSubmit = async (e) => {
|
|
15
17
|
e.preventDefault();
|
|
@@ -18,6 +20,7 @@ export const CreateApiKeyModal = ({ isOpen, onClose, onCreateKey, theme, }) => {
|
|
|
18
20
|
try {
|
|
19
21
|
const result = await onCreateKey({
|
|
20
22
|
name: keyName,
|
|
23
|
+
graphId: scopeGraphId || undefined,
|
|
21
24
|
});
|
|
22
25
|
setNewApiKey(result);
|
|
23
26
|
}
|
|
@@ -31,6 +34,7 @@ export const CreateApiKeyModal = ({ isOpen, onClose, onCreateKey, theme, }) => {
|
|
|
31
34
|
const handleCloseModal = () => {
|
|
32
35
|
onClose();
|
|
33
36
|
setKeyName('');
|
|
37
|
+
setScopeGraphId('');
|
|
34
38
|
setNewApiKey(null);
|
|
35
39
|
setError(null);
|
|
36
40
|
};
|
|
@@ -43,5 +47,7 @@ export const CreateApiKeyModal = ({ isOpen, onClose, onCreateKey, theme, }) => {
|
|
|
43
47
|
day: 'numeric',
|
|
44
48
|
});
|
|
45
49
|
};
|
|
46
|
-
return (_jsxs(Modal, { theme: theme === null || theme === void 0 ? void 0 : theme.modal, show: isOpen, onClose: handleCloseModal, children: [_jsx(ModalHeader, { children: "Create API Key" }), _jsx(ModalBody, { children: newApiKey ? (_jsxs("div", { children: [_jsx(StatusAlert, { type: "success", message: "Your API key has been created.", theme: theme === null || theme === void 0 ? void 0 : theme.alert }), _jsxs("div", { className: "mb-4 space-y-4", children: [_jsx(ApiKeyDisplay, { label: "ROBOSYSTEMS_API_KEY", value: newApiKey.key, keyId: newApiKey.id, theme: theme }), _jsx("p", { className: "text-sm font-medium text-red-600 dark:text-red-400", children: "\u26A0\uFE0F Important: Save this API key somewhere safe. For security reasons, you cannot view it again after closing this dialog." })] }), _jsxs("div", { className: "mb-2", children: [_jsx(Label, { theme: theme === null || theme === void 0 ? void 0 : theme.label, htmlFor: "keyDetails", children: "Key Details" }), _jsxs("div", { className: "mt-2 space-y-2 rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-800", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Name:" }), _jsx("span", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: newApiKey.name })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Created:" }), _jsx("span", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: formatDate(newApiKey.createdAt) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Status:" }), _jsx("span", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Active" })] })] })] })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsx(SettingsFormField, { id: "keyName", label: "API Key Name", placeholder: "My API Key", value: keyName, onChange: (e) => setKeyName(e.target.value), required: true, theme: theme }), _jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Give your API key a memorable name to identify it later" }),
|
|
50
|
+
return (_jsxs(Modal, { theme: theme === null || theme === void 0 ? void 0 : theme.modal, show: isOpen, onClose: handleCloseModal, children: [_jsx(ModalHeader, { children: "Create API Key" }), _jsx(ModalBody, { children: newApiKey ? (_jsxs("div", { children: [_jsx(StatusAlert, { type: "success", message: "Your API key has been created.", theme: theme === null || theme === void 0 ? void 0 : theme.alert }), _jsxs("div", { className: "mb-4 space-y-4", children: [_jsx(ApiKeyDisplay, { label: "ROBOSYSTEMS_API_KEY", value: newApiKey.key, keyId: newApiKey.id, theme: theme }), _jsx("p", { className: "text-sm font-medium text-red-600 dark:text-red-400", children: "\u26A0\uFE0F Important: Save this API key somewhere safe. For security reasons, you cannot view it again after closing this dialog." })] }), _jsxs("div", { className: "mb-2", children: [_jsx(Label, { theme: theme === null || theme === void 0 ? void 0 : theme.label, htmlFor: "keyDetails", children: "Key Details" }), _jsxs("div", { className: "mt-2 space-y-2 rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-800", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Name:" }), _jsx("span", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: newApiKey.name })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Scope:" }), _jsx("span", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: (_a = newApiKey.graphId) !== null && _a !== void 0 ? _a : 'Account-wide' })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Created:" }), _jsx("span", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: formatDate(newApiKey.createdAt) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Status:" }), _jsx("span", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Active" })] })] })] })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsx(SettingsFormField, { id: "keyName", label: "API Key Name", placeholder: "My API Key", value: keyName, onChange: (e) => setKeyName(e.target.value), required: true, theme: theme }), _jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Give your API key a memorable name to identify it later" }), graphs && graphs.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { theme: theme === null || theme === void 0 ? void 0 : theme.label, htmlFor: "keyScope", children: "Scope" }), _jsxs(Select, { id: "keyScope", theme: theme === null || theme === void 0 ? void 0 : theme.select, value: scopeGraphId, onChange: (e) => setScopeGraphId(e.target.value), children: [_jsx("option", { value: "", children: "Account-wide (all graphs)" }), graphs.map((graph) => (_jsxs("option", { value: graph.graphId, children: [graph.graphName, " (", graph.graphId, ")"] }, graph.graphId)))] }), _jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: scopeGraphId
|
|
51
|
+
? 'Graph-scoped: works only for this graph (and its subgraphs), and is the only kind of key accepted inside an MCP connector URL.'
|
|
52
|
+
: 'Account-wide: works for every graph via the X-API-Key header and the SDKs. Not accepted inside MCP connector URLs — pick a graph scope for that, or use the Connect page.' })] })), _jsx("div", { className: "bg-primary-50 dark:bg-primary-900/30 rounded-lg p-3", children: _jsxs("p", { className: "text-primary-800 dark:text-primary-300 text-sm", children: [_jsx("strong", { children: "Note:" }), " API keys do not expire by date. Keys that haven't been used for 90 days will be automatically deactivated for security."] }) }), error && (_jsx(StatusAlert, { type: "error", message: error, theme: theme === null || theme === void 0 ? void 0 : theme.alert }))] })) }), _jsx(ModalFooter, { children: newApiKey ? (_jsx(Button, { theme: theme === null || theme === void 0 ? void 0 : theme.button, color: "blue", onClick: handleCloseModal, children: "Done" })) : (_jsxs(_Fragment, { children: [_jsx(Button, { theme: theme === null || theme === void 0 ? void 0 : theme.button, color: "gray", onClick: handleCloseModal, children: "Cancel" }), _jsx(Button, { theme: theme === null || theme === void 0 ? void 0 : theme.button, color: "primary", onClick: handleSubmit, disabled: isCreating || !keyName, children: isCreating ? (_jsx(Spinner, { size: "sm", className: "text-white" })) : ('Create API Key') })] })) })] }));
|
|
47
53
|
};
|
|
@@ -4,6 +4,8 @@ import * as SDK from '@robosystems/client';
|
|
|
4
4
|
import { Button } from 'flowbite-react';
|
|
5
5
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
6
|
import { HiLockClosed } from 'react-icons/hi';
|
|
7
|
+
import { useOptionalAuth } from '../../auth-components/AuthProvider';
|
|
8
|
+
import { ConfirmModal } from '../ConfirmModal';
|
|
7
9
|
import { Spinner } from '../Spinner';
|
|
8
10
|
import { SettingsCard } from '../forms/SettingsCard';
|
|
9
11
|
import { SettingsFormField } from '../forms/SettingsFormField';
|
|
@@ -15,7 +17,10 @@ export const PasswordInformationCard = ({ theme, onUpdate = undefined, onSuccess
|
|
|
15
17
|
const [newPassword, setNewPassword] = useState('');
|
|
16
18
|
const [passwordStrength, setPasswordStrength] = useState(null);
|
|
17
19
|
const [checkingPassword, setCheckingPassword] = useState(false);
|
|
20
|
+
const [pendingUpdate, setPendingUpdate] = useState(null);
|
|
18
21
|
const debounceRef = useRef(null);
|
|
22
|
+
const formRef = useRef(null);
|
|
23
|
+
const auth = useOptionalAuth();
|
|
19
24
|
const checkPassword = useCallback(async (password) => {
|
|
20
25
|
if (password.length < 4) {
|
|
21
26
|
setPasswordStrength(null);
|
|
@@ -57,8 +62,7 @@ export const PasswordInformationCard = ({ theme, onUpdate = undefined, onSuccess
|
|
|
57
62
|
clearTimeout(debounceRef.current);
|
|
58
63
|
};
|
|
59
64
|
}, [newPassword, checkPassword]);
|
|
60
|
-
const handleSubmit =
|
|
61
|
-
var _a, _b, _c;
|
|
65
|
+
const handleSubmit = (event) => {
|
|
62
66
|
event.preventDefault();
|
|
63
67
|
setError(null);
|
|
64
68
|
setSuccess(false);
|
|
@@ -95,18 +99,25 @@ export const PasswordInformationCard = ({ theme, onUpdate = undefined, onSuccess
|
|
|
95
99
|
}
|
|
96
100
|
return;
|
|
97
101
|
}
|
|
102
|
+
// A password change ends every session, this one included, so take an
|
|
103
|
+
// explicit confirmation before doing something the user can't walk back.
|
|
104
|
+
setPendingUpdate(updateData);
|
|
105
|
+
};
|
|
106
|
+
const handleConfirm = async () => {
|
|
107
|
+
var _a, _b, _c, _d;
|
|
108
|
+
if (!pendingUpdate)
|
|
109
|
+
return;
|
|
98
110
|
setIsLoading(true);
|
|
99
|
-
const form = event.currentTarget;
|
|
100
111
|
try {
|
|
101
112
|
if (onUpdate) {
|
|
102
|
-
await onUpdate(
|
|
113
|
+
await onUpdate(pendingUpdate);
|
|
103
114
|
}
|
|
104
115
|
else {
|
|
105
116
|
const response = await SDK.updateUserPassword({
|
|
106
117
|
body: {
|
|
107
|
-
current_password:
|
|
108
|
-
new_password:
|
|
109
|
-
confirm_password:
|
|
118
|
+
current_password: pendingUpdate.currentPassword,
|
|
119
|
+
new_password: pendingUpdate.newPassword,
|
|
120
|
+
confirm_password: pendingUpdate.confirmPassword,
|
|
110
121
|
},
|
|
111
122
|
});
|
|
112
123
|
if (response.error) {
|
|
@@ -116,14 +127,28 @@ export const PasswordInformationCard = ({ theme, onUpdate = undefined, onSuccess
|
|
|
116
127
|
throw new Error(typeof detail === 'string' ? detail : 'Failed to update password.');
|
|
117
128
|
}
|
|
118
129
|
}
|
|
130
|
+
setPendingUpdate(null);
|
|
119
131
|
if (onSuccess) {
|
|
120
132
|
onSuccess('Password updated successfully.');
|
|
121
133
|
}
|
|
122
134
|
else {
|
|
123
135
|
setSuccess(true);
|
|
124
136
|
}
|
|
137
|
+
// The server invalidates every session on a password change, so the token
|
|
138
|
+
// this app holds is already dead. Sign out deliberately instead of
|
|
139
|
+
// letting it fail on the next request as an unexplained error.
|
|
140
|
+
if (auth) {
|
|
141
|
+
try {
|
|
142
|
+
await auth.logout('password_changed');
|
|
143
|
+
return; // navigating away — leave the card as-is
|
|
144
|
+
}
|
|
145
|
+
catch (_e) {
|
|
146
|
+
// The password did change; only the redirect failed. Fall through so
|
|
147
|
+
// the card resets rather than sitting on a spinner.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
125
150
|
// Clear form fields and strength state
|
|
126
|
-
|
|
151
|
+
(_d = formRef.current) === null || _d === void 0 ? void 0 : _d.reset();
|
|
127
152
|
setNewPassword('');
|
|
128
153
|
setPasswordStrength(null);
|
|
129
154
|
setTimeout(() => {
|
|
@@ -141,6 +166,7 @@ export const PasswordInformationCard = ({ theme, onUpdate = undefined, onSuccess
|
|
|
141
166
|
else {
|
|
142
167
|
setError(msg);
|
|
143
168
|
}
|
|
169
|
+
setPendingUpdate(null);
|
|
144
170
|
setIsLoading(false);
|
|
145
171
|
}
|
|
146
172
|
};
|
|
@@ -162,6 +188,6 @@ export const PasswordInformationCard = ({ theme, onUpdate = undefined, onSuccess
|
|
|
162
188
|
: passwordStrength.score < 80
|
|
163
189
|
? 'text-primary-300'
|
|
164
190
|
: 'text-green-400';
|
|
165
|
-
return (_jsxs(SettingsCard, { title: "Password information", description: "Update your password to keep your account secure", icon: HiLockClosed, theme: theme === null || theme === void 0 ? void 0 : theme.card, className: className, children: [_jsxs("div", { className: "space-y-4", children: [error && (_jsx(StatusAlert, { type: "error", message: error, theme: theme === null || theme === void 0 ? void 0 : theme.alert })), success && (_jsx(StatusAlert, { type: "success", message: "Password updated successfully.", theme: theme === null || theme === void 0 ? void 0 : theme.alert }))] }), _jsx("form", { onSubmit: handleSubmit, children: _jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "grid gap-4 md:grid-cols-2", children: _jsx(SettingsFormField, { id: "currentPassword", label: "Current password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", theme: theme }) }), _jsxs("div", { className: "grid items-start gap-4 md:grid-cols-2", children: [_jsxs("div", { children: [_jsx(SettingsFormField, { id: "newPassword", label: "New password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", theme: theme, onChange: (e) => setNewPassword(e.target.value) }), _jsx("div", { className: "mt-2 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: `h-full rounded-full transition-all duration-300 ${strengthBarColor}`, style: { width: `${passwordStrength.score}%` } }) }), _jsx("span", { className: `text-xs font-medium capitalize ${strengthTextColor}`, children: passwordStrength.strength.replace('-', ' ') })] }), passwordStrength.errors.length > 0 && (_jsx("p", { className: "text-xs text-red-400", children: passwordStrength.errors[0] })), passwordStrength.errors.length === 0 &&
|
|
166
|
-
passwordStrength.suggestions.length > 0 && (_jsx("p", { className: "text-xs text-gray-400", children: passwordStrength.suggestions[0] }))] })) })] }), _jsx(SettingsFormField, { id: "confirmPassword", label: "Confirm password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", theme: theme })] }), _jsx("div", { className: "flex justify-end pt-2", children: _jsx(Button, { theme: theme === null || theme === void 0 ? void 0 : theme.button, color: "blue", type: "submit", disabled: isLoading || checkingPassword, children: isLoading ? (_jsx(Spinner, { size: "sm", className: "text-white" })) : ('Update Password') }) })] }) })] }));
|
|
191
|
+
return (_jsxs(SettingsCard, { title: "Password information", description: "Update your password to keep your account secure", icon: HiLockClosed, theme: theme === null || theme === void 0 ? void 0 : theme.card, className: className, children: [_jsxs("div", { className: "space-y-4", children: [error && (_jsx(StatusAlert, { type: "error", message: error, theme: theme === null || theme === void 0 ? void 0 : theme.alert })), success && (_jsx(StatusAlert, { type: "success", message: "Password updated successfully.", theme: theme === null || theme === void 0 ? void 0 : theme.alert }))] }), _jsx("form", { ref: formRef, onSubmit: handleSubmit, children: _jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "grid gap-4 md:grid-cols-2", children: _jsx(SettingsFormField, { id: "currentPassword", label: "Current password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", theme: theme }) }), _jsxs("div", { className: "grid items-start gap-4 md:grid-cols-2", children: [_jsxs("div", { children: [_jsx(SettingsFormField, { id: "newPassword", label: "New password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", theme: theme, onChange: (e) => setNewPassword(e.target.value) }), _jsx("div", { className: "mt-2 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: `h-full rounded-full transition-all duration-300 ${strengthBarColor}`, style: { width: `${passwordStrength.score}%` } }) }), _jsx("span", { className: `text-xs font-medium capitalize ${strengthTextColor}`, children: passwordStrength.strength.replace('-', ' ') })] }), passwordStrength.errors.length > 0 && (_jsx("p", { className: "text-xs text-red-400", children: passwordStrength.errors[0] })), passwordStrength.errors.length === 0 &&
|
|
192
|
+
passwordStrength.suggestions.length > 0 && (_jsx("p", { className: "text-xs text-gray-400", children: passwordStrength.suggestions[0] }))] })) })] }), _jsx(SettingsFormField, { id: "confirmPassword", label: "Confirm password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", theme: theme })] }), _jsx("div", { className: "flex justify-end pt-2", children: _jsx(Button, { theme: theme === null || theme === void 0 ? void 0 : theme.button, color: "blue", type: "submit", disabled: isLoading || checkingPassword, children: isLoading ? (_jsx(Spinner, { size: "sm", className: "text-white" })) : ('Update Password') }) })] }) }), _jsx(ConfirmModal, { show: pendingUpdate !== null, onClose: () => setPendingUpdate(null), onConfirm: handleConfirm, loading: isLoading, title: "Change password", confirmLabel: "Change password", loadingLabel: "Changing\u2026", confirmColor: "blue", confirmIcon: HiLockClosed, children: _jsx("p", { className: "text-sm text-gray-300", children: "Changing your password signs you out everywhere, including on this device. You'll need to sign in again with your new password." }) })] }));
|
|
167
193
|
};
|