@chaaskit/client 0.1.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/dist/favicon.svg +11 -0
- package/dist/index.html +17 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
- package/dist/lib/extensions.js +10 -0
- package/dist/lib/extensions.js.map +1 -0
- package/dist/lib/favicon.svg +11 -0
- package/dist/lib/index.js +74126 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logo.svg +12 -0
- package/dist/lib/routes/AcceptInviteRoute.js +19 -0
- package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
- package/dist/lib/routes/AdminDashboardRoute.js +19 -0
- package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamRoute.js +19 -0
- package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamsRoute.js +19 -0
- package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
- package/dist/lib/routes/AdminUsersRoute.js +19 -0
- package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
- package/dist/lib/routes/ApiKeysRoute.js +19 -0
- package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
- package/dist/lib/routes/AutomationsRoute.js +19 -0
- package/dist/lib/routes/AutomationsRoute.js.map +1 -0
- package/dist/lib/routes/ChatRoute.js +19 -0
- package/dist/lib/routes/ChatRoute.js.map +1 -0
- package/dist/lib/routes/DocumentsRoute.js +19 -0
- package/dist/lib/routes/DocumentsRoute.js.map +1 -0
- package/dist/lib/routes/OAuthConsentRoute.js +19 -0
- package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
- package/dist/lib/routes/PricingRoute.js +19 -0
- package/dist/lib/routes/PricingRoute.js.map +1 -0
- package/dist/lib/routes/PrivacyRoute.js +19 -0
- package/dist/lib/routes/PrivacyRoute.js.map +1 -0
- package/dist/lib/routes/TeamSettingsRoute.js +19 -0
- package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
- package/dist/lib/routes/TermsRoute.js +19 -0
- package/dist/lib/routes/TermsRoute.js.map +1 -0
- package/dist/lib/routes/VerifyEmailRoute.js +19 -0
- package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
- package/dist/lib/routes.js +79 -0
- package/dist/lib/routes.js.map +1 -0
- package/dist/lib/ssr-utils.js +29 -0
- package/dist/lib/ssr-utils.js.map +1 -0
- package/dist/lib/ssr.js +60 -0
- package/dist/lib/ssr.js.map +1 -0
- package/dist/lib/styles.css +2410 -0
- package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
- package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
- package/dist/logo.svg +12 -0
- package/package.json +84 -0
- package/src/components/AgentSelector.tsx +90 -0
- package/src/components/BranchModal.tsx +129 -0
- package/src/components/ClientOnly.tsx +27 -0
- package/src/components/ExportMenu.tsx +122 -0
- package/src/components/LoadingSkeletons.tsx +110 -0
- package/src/components/MCPCredentialsSection.tsx +309 -0
- package/src/components/MentionChip.tsx +149 -0
- package/src/components/MentionDropdown.tsx +175 -0
- package/src/components/MentionInput.tsx +293 -0
- package/src/components/MessageItem.tsx +300 -0
- package/src/components/MessageList.tsx +159 -0
- package/src/components/OAuthAppsSection.tsx +124 -0
- package/src/components/ProjectFolder.tsx +141 -0
- package/src/components/ProjectModal.tsx +296 -0
- package/src/components/SSRMessageList.tsx +153 -0
- package/src/components/SearchModal.tsx +173 -0
- package/src/components/SettingsModal.tsx +412 -0
- package/src/components/ShareModal.tsx +280 -0
- package/src/components/Sidebar.tsx +491 -0
- package/src/components/TeamSwitcher.tsx +273 -0
- package/src/components/ToolCallDisplay.tsx +473 -0
- package/src/components/ToolConfirmationModal.tsx +130 -0
- package/src/components/UsageChart.tsx +177 -0
- package/src/components/content/CodeBlock.tsx +69 -0
- package/src/components/content/MarkdownRenderer.tsx +64 -0
- package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
- package/src/contexts/AuthContext.tsx +119 -0
- package/src/contexts/ConfigContext.tsx +214 -0
- package/src/contexts/ProjectContext.tsx +167 -0
- package/src/contexts/ServerConfigProvider.tsx +41 -0
- package/src/contexts/ServerThemeProvider.tsx +47 -0
- package/src/contexts/TeamContext.tsx +255 -0
- package/src/contexts/ThemeContext.tsx +113 -0
- package/src/extensions/index.ts +15 -0
- package/src/extensions/registry.ts +187 -0
- package/src/extensions/useExtensions.ts +52 -0
- package/src/hooks/useAppPath.ts +34 -0
- package/src/hooks/useBasePath.ts +13 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -0
- package/src/hooks/useMentionSearch.ts +106 -0
- package/src/index.tsx +116 -0
- package/src/layouts/MainLayout.tsx +98 -0
- package/src/pages/AcceptInvitePage.tsx +175 -0
- package/src/pages/AdminDashboardPage.tsx +362 -0
- package/src/pages/AdminTeamPage.tsx +304 -0
- package/src/pages/AdminTeamsPage.tsx +242 -0
- package/src/pages/AdminUsersPage.tsx +385 -0
- package/src/pages/ApiKeysPage.tsx +449 -0
- package/src/pages/ChatPage.tsx +310 -0
- package/src/pages/DocumentsPage.tsx +577 -0
- package/src/pages/LoginPage.tsx +232 -0
- package/src/pages/OAuthConsentPage.tsx +234 -0
- package/src/pages/PricingPage.tsx +314 -0
- package/src/pages/PrivacyPage.tsx +65 -0
- package/src/pages/RegisterPage.tsx +153 -0
- package/src/pages/ScheduledPromptsPage.tsx +702 -0
- package/src/pages/SharedThreadPage.tsx +116 -0
- package/src/pages/TeamSettingsPage.tsx +1085 -0
- package/src/pages/TermsPage.tsx +82 -0
- package/src/pages/VerifyEmailPage.tsx +202 -0
- package/src/routes/AcceptInviteRoute.tsx +24 -0
- package/src/routes/AdminDashboardRoute.tsx +24 -0
- package/src/routes/AdminTeamRoute.tsx +24 -0
- package/src/routes/AdminTeamsRoute.tsx +24 -0
- package/src/routes/AdminUsersRoute.tsx +24 -0
- package/src/routes/ApiKeysRoute.tsx +24 -0
- package/src/routes/AutomationsRoute.tsx +24 -0
- package/src/routes/ChatRoute.tsx +28 -0
- package/src/routes/DocumentsRoute.tsx +24 -0
- package/src/routes/OAuthConsentRoute.tsx +24 -0
- package/src/routes/PricingRoute.tsx +24 -0
- package/src/routes/PrivacyRoute.tsx +24 -0
- package/src/routes/TeamSettingsRoute.tsx +24 -0
- package/src/routes/TermsRoute.tsx +24 -0
- package/src/routes/VerifyEmailRoute.tsx +24 -0
- package/src/routes/index.ts +57 -0
- package/src/ssr-utils.tsx +84 -0
- package/src/ssr.ts +123 -0
- package/src/stores/chatStore.ts +670 -0
- package/src/styles/index.css +254 -0
- package/src/utils/api.ts +78 -0
- package/src/vite-env.d.ts +13 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Link, useNavigate } from 'react-router';
|
|
3
|
+
import { useAuth } from '../contexts/AuthContext';
|
|
4
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
5
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
6
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
7
|
+
|
|
8
|
+
export default function LoginPage() {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const appPath = useAppPath();
|
|
11
|
+
const { login, sendMagicLink } = useAuth();
|
|
12
|
+
const config = useConfig();
|
|
13
|
+
const { theme } = useTheme();
|
|
14
|
+
|
|
15
|
+
const [email, setEmail] = useState('');
|
|
16
|
+
const [password, setPassword] = useState('');
|
|
17
|
+
const [error, setError] = useState('');
|
|
18
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
19
|
+
const [magicLinkSent, setMagicLinkSent] = useState(false);
|
|
20
|
+
|
|
21
|
+
const hasEmailPassword = config.auth.methods.includes('email-password');
|
|
22
|
+
const hasMagicLink = config.auth.magicLink.enabled;
|
|
23
|
+
const hasGoogle = config.auth.methods.includes('google');
|
|
24
|
+
const hasGitHub = config.auth.methods.includes('github');
|
|
25
|
+
|
|
26
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
setError('');
|
|
29
|
+
setIsLoading(true);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { requiresVerification } = await login(email, password);
|
|
33
|
+
if (requiresVerification) {
|
|
34
|
+
navigate('/verify-email');
|
|
35
|
+
} else {
|
|
36
|
+
navigate(appPath('/'));
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setError(err instanceof Error ? err.message : 'Login failed');
|
|
40
|
+
} finally {
|
|
41
|
+
setIsLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleMagicLink() {
|
|
46
|
+
if (!email) {
|
|
47
|
+
setError('Please enter your email');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setError('');
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await sendMagicLink(email);
|
|
56
|
+
setMagicLinkSent(true);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setError(err instanceof Error ? err.message : 'Failed to send magic link');
|
|
59
|
+
} finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (magicLinkSent) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
|
67
|
+
<div className="w-full max-w-md text-center">
|
|
68
|
+
<h1 className="mb-4 text-2xl font-bold text-text-primary">
|
|
69
|
+
Check your email
|
|
70
|
+
</h1>
|
|
71
|
+
<p className="mb-6 text-text-secondary">
|
|
72
|
+
We've sent a magic link to <strong>{email}</strong>. Click the link
|
|
73
|
+
in the email to sign in.
|
|
74
|
+
</p>
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => setMagicLinkSent(false)}
|
|
77
|
+
className="text-primary hover:underline"
|
|
78
|
+
>
|
|
79
|
+
Use a different email
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
|
88
|
+
<div className="w-full max-w-md">
|
|
89
|
+
<div className="mb-8 text-center">
|
|
90
|
+
{config.ui.logo && (
|
|
91
|
+
<img
|
|
92
|
+
src={typeof config.ui.logo === 'string' ? config.ui.logo : (theme === 'dark' ? config.ui.logo.dark : config.ui.logo.light)}
|
|
93
|
+
alt={config.app.name}
|
|
94
|
+
className="mx-auto mb-4 h-16 w-16 rounded-lg object-contain"
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
<h1 className="text-3xl font-bold text-text-primary">Welcome back</h1>
|
|
98
|
+
<p className="mt-2 text-text-secondary">Sign in to {config.app.name}</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{error && (
|
|
102
|
+
<div className="mb-4 rounded-lg bg-error/10 p-3 text-sm text-error">
|
|
103
|
+
{error}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{/* OAuth buttons */}
|
|
108
|
+
{(hasGoogle || hasGitHub) && (
|
|
109
|
+
<div className="mb-6 space-y-3">
|
|
110
|
+
{hasGoogle && (
|
|
111
|
+
<a
|
|
112
|
+
href="/api/auth/oauth/google"
|
|
113
|
+
className="flex w-full items-center justify-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-text-primary hover:bg-background-secondary"
|
|
114
|
+
>
|
|
115
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
116
|
+
<path
|
|
117
|
+
fill="currentColor"
|
|
118
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
119
|
+
/>
|
|
120
|
+
<path
|
|
121
|
+
fill="currentColor"
|
|
122
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
123
|
+
/>
|
|
124
|
+
<path
|
|
125
|
+
fill="currentColor"
|
|
126
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
127
|
+
/>
|
|
128
|
+
<path
|
|
129
|
+
fill="currentColor"
|
|
130
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
Continue with Google
|
|
134
|
+
</a>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{hasGitHub && (
|
|
138
|
+
<a
|
|
139
|
+
href="/api/auth/oauth/github"
|
|
140
|
+
className="flex w-full items-center justify-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-text-primary hover:bg-background-secondary"
|
|
141
|
+
>
|
|
142
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
|
143
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
144
|
+
</svg>
|
|
145
|
+
Continue with GitHub
|
|
146
|
+
</a>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{(hasGoogle || hasGitHub) && hasEmailPassword && (
|
|
152
|
+
<div className="relative mb-6">
|
|
153
|
+
<div className="absolute inset-0 flex items-center">
|
|
154
|
+
<div className="w-full border-t border-border" />
|
|
155
|
+
</div>
|
|
156
|
+
<div className="relative flex justify-center text-sm">
|
|
157
|
+
<span className="bg-background px-2 text-text-muted">
|
|
158
|
+
Or continue with email
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Email/Password form */}
|
|
165
|
+
{hasEmailPassword && (
|
|
166
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
167
|
+
<div>
|
|
168
|
+
<label
|
|
169
|
+
htmlFor="email"
|
|
170
|
+
className="block text-sm font-medium text-text-primary"
|
|
171
|
+
>
|
|
172
|
+
Email
|
|
173
|
+
</label>
|
|
174
|
+
<input
|
|
175
|
+
type="email"
|
|
176
|
+
id="email"
|
|
177
|
+
value={email}
|
|
178
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
179
|
+
required
|
|
180
|
+
className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div>
|
|
185
|
+
<label
|
|
186
|
+
htmlFor="password"
|
|
187
|
+
className="block text-sm font-medium text-text-primary"
|
|
188
|
+
>
|
|
189
|
+
Password
|
|
190
|
+
</label>
|
|
191
|
+
<input
|
|
192
|
+
type="password"
|
|
193
|
+
id="password"
|
|
194
|
+
value={password}
|
|
195
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
196
|
+
required
|
|
197
|
+
className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<button
|
|
202
|
+
type="submit"
|
|
203
|
+
disabled={isLoading}
|
|
204
|
+
className="w-full rounded-lg bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
205
|
+
>
|
|
206
|
+
{isLoading ? 'Signing in...' : 'Sign in'}
|
|
207
|
+
</button>
|
|
208
|
+
</form>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{/* Magic Link */}
|
|
212
|
+
{hasMagicLink && hasEmailPassword && (
|
|
213
|
+
<button
|
|
214
|
+
onClick={handleMagicLink}
|
|
215
|
+
disabled={isLoading}
|
|
216
|
+
className="mt-4 w-full text-center text-sm text-primary hover:underline disabled:opacity-50"
|
|
217
|
+
>
|
|
218
|
+
Sign in with magic link instead
|
|
219
|
+
</button>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{/* Register link */}
|
|
223
|
+
<p className="mt-6 text-center text-sm text-text-secondary">
|
|
224
|
+
Don't have an account?{' '}
|
|
225
|
+
<Link to="/register" className="text-primary hover:underline">
|
|
226
|
+
Sign up
|
|
227
|
+
</Link>
|
|
228
|
+
</p>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useSearchParams, useNavigate, Navigate } from 'react-router';
|
|
3
|
+
import { Shield, Check, X, Loader2, ExternalLink } from 'lucide-react';
|
|
4
|
+
import { useAuth } from '../contexts/AuthContext';
|
|
5
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
6
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
7
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
8
|
+
|
|
9
|
+
interface ClientInfo {
|
|
10
|
+
clientId: string;
|
|
11
|
+
clientName: string;
|
|
12
|
+
clientUri?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function OAuthConsentPage() {
|
|
16
|
+
const [searchParams] = useSearchParams();
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
const appPath = useAppPath();
|
|
19
|
+
const { user, isLoading: authLoading } = useAuth();
|
|
20
|
+
const config = useConfig();
|
|
21
|
+
const { theme } = useTheme();
|
|
22
|
+
|
|
23
|
+
const [clientInfo, setClientInfo] = useState<ClientInfo | null>(null);
|
|
24
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
25
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
26
|
+
const [error, setError] = useState<string | null>(null);
|
|
27
|
+
|
|
28
|
+
// Get params from URL
|
|
29
|
+
const clientId = searchParams.get('client_id');
|
|
30
|
+
const redirectUri = searchParams.get('redirect_uri');
|
|
31
|
+
const scope = searchParams.get('scope');
|
|
32
|
+
const state = searchParams.get('state');
|
|
33
|
+
const codeChallenge = searchParams.get('code_challenge');
|
|
34
|
+
const codeChallengeMethod = searchParams.get('code_challenge_method');
|
|
35
|
+
|
|
36
|
+
// Get logo
|
|
37
|
+
const logo = typeof config.ui.logo === 'string'
|
|
38
|
+
? config.ui.logo
|
|
39
|
+
: (theme === 'dark' ? config.ui.logo.dark : config.ui.logo.light);
|
|
40
|
+
|
|
41
|
+
// Load client info
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
async function loadClientInfo() {
|
|
44
|
+
if (!clientId) {
|
|
45
|
+
setError('Missing client_id parameter');
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// For now, we'll just display the client_id
|
|
52
|
+
// In a full implementation, we'd fetch client details from the server
|
|
53
|
+
setClientInfo({
|
|
54
|
+
clientId,
|
|
55
|
+
clientName: clientId.startsWith('mcp_') ? 'MCP Client' : clientId,
|
|
56
|
+
});
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
setError('Failed to load client information');
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
loadClientInfo();
|
|
65
|
+
}, [clientId]);
|
|
66
|
+
|
|
67
|
+
// Handle consent decision
|
|
68
|
+
async function handleConsent(approved: boolean) {
|
|
69
|
+
setIsSubmitting(true);
|
|
70
|
+
setError(null);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// OAuth endpoints are at the API root level, not under /api
|
|
74
|
+
// In dev mode with separate servers, detect localhost:5173 and route to localhost:3000
|
|
75
|
+
// In production, same-origin requests work since API and client are served together
|
|
76
|
+
let apiUrl = import.meta.env.VITE_API_URL || '';
|
|
77
|
+
if (!apiUrl && window.location.hostname === 'localhost' && window.location.port === '5173') {
|
|
78
|
+
apiUrl = 'http://localhost:3000';
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(`${apiUrl}/oauth/authorize`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
credentials: 'include',
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
client_id: clientId,
|
|
86
|
+
redirect_uri: redirectUri,
|
|
87
|
+
scope,
|
|
88
|
+
state,
|
|
89
|
+
code_challenge: codeChallenge,
|
|
90
|
+
code_challenge_method: codeChallengeMethod || 'S256',
|
|
91
|
+
consent: approved ? 'approve' : 'deny',
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const error = await response.json().catch(() => ({ message: 'Request failed' }));
|
|
97
|
+
throw new Error(error.error?.message || error.message || 'Authorization failed');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = await response.json() as { redirect: string };
|
|
101
|
+
// Redirect to the client's redirect URI
|
|
102
|
+
window.location.href = data.redirect;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
setError(err instanceof Error ? err.message : 'Authorization failed');
|
|
105
|
+
setIsSubmitting(false);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Redirect to login if not authenticated
|
|
110
|
+
if (!authLoading && !user) {
|
|
111
|
+
const returnUrl = `/oauth/consent?${searchParams.toString()}`;
|
|
112
|
+
return <Navigate to={`/login?returnTo=${encodeURIComponent(returnUrl)}`} replace />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse scopes for display
|
|
116
|
+
const scopeList = scope ? scope.split(' ') : [];
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-4">
|
|
120
|
+
<div className="w-full max-w-md">
|
|
121
|
+
{/* Logo */}
|
|
122
|
+
<div className="mb-8 text-center">
|
|
123
|
+
<img
|
|
124
|
+
src={logo}
|
|
125
|
+
alt={config.app.name}
|
|
126
|
+
className="mx-auto h-12 w-auto"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Consent Card */}
|
|
131
|
+
<div className="rounded-2xl border border-border bg-background-secondary p-6 shadow-lg">
|
|
132
|
+
{isLoading || authLoading ? (
|
|
133
|
+
<div className="flex items-center justify-center py-8">
|
|
134
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
135
|
+
</div>
|
|
136
|
+
) : error ? (
|
|
137
|
+
<div className="text-center">
|
|
138
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-error/10">
|
|
139
|
+
<X className="h-6 w-6 text-error" />
|
|
140
|
+
</div>
|
|
141
|
+
<h2 className="text-lg font-semibold text-text-primary">Authorization Error</h2>
|
|
142
|
+
<p className="mt-2 text-sm text-text-secondary">{error}</p>
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => navigate(appPath('/'))}
|
|
145
|
+
className="mt-4 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
|
146
|
+
>
|
|
147
|
+
Return Home
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
) : (
|
|
151
|
+
<>
|
|
152
|
+
{/* Header */}
|
|
153
|
+
<div className="mb-6 text-center">
|
|
154
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
|
155
|
+
<Shield className="h-6 w-6 text-primary" />
|
|
156
|
+
</div>
|
|
157
|
+
<h2 className="text-lg font-semibold text-text-primary">
|
|
158
|
+
Authorize Application
|
|
159
|
+
</h2>
|
|
160
|
+
<p className="mt-1 text-sm text-text-secondary">
|
|
161
|
+
<span className="font-medium text-text-primary">{clientInfo?.clientName}</span>
|
|
162
|
+
{' '}wants to access your account
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Client Info */}
|
|
167
|
+
{clientInfo?.clientUri && (
|
|
168
|
+
<a
|
|
169
|
+
href={clientInfo.clientUri}
|
|
170
|
+
target="_blank"
|
|
171
|
+
rel="noopener noreferrer"
|
|
172
|
+
className="mb-4 flex items-center justify-center gap-1 text-sm text-primary hover:underline"
|
|
173
|
+
>
|
|
174
|
+
Visit application website
|
|
175
|
+
<ExternalLink size={14} />
|
|
176
|
+
</a>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{/* Scopes */}
|
|
180
|
+
{scopeList.length > 0 && (
|
|
181
|
+
<div className="mb-6 rounded-lg border border-border bg-background p-4">
|
|
182
|
+
<p className="mb-3 text-sm font-medium text-text-primary">
|
|
183
|
+
This application will be able to:
|
|
184
|
+
</p>
|
|
185
|
+
<ul className="space-y-2">
|
|
186
|
+
{scopeList.map((s) => (
|
|
187
|
+
<li key={s} className="flex items-center gap-2 text-sm text-text-secondary">
|
|
188
|
+
<Check size={16} className="text-success" />
|
|
189
|
+
{s === 'mcp:tools' && 'Use available tools on your behalf'}
|
|
190
|
+
{s === 'mcp:resources' && 'Read resources on your behalf'}
|
|
191
|
+
{!['mcp:tools', 'mcp:resources'].includes(s) && s}
|
|
192
|
+
</li>
|
|
193
|
+
))}
|
|
194
|
+
</ul>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{/* User Info */}
|
|
199
|
+
<div className="mb-6 rounded-lg border border-border bg-background p-3">
|
|
200
|
+
<p className="text-xs text-text-muted">Authorizing as</p>
|
|
201
|
+
<p className="text-sm font-medium text-text-primary">{user?.email}</p>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Actions */}
|
|
205
|
+
<div className="flex gap-3">
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => handleConsent(false)}
|
|
208
|
+
disabled={isSubmitting}
|
|
209
|
+
className="flex-1 rounded-lg border border-border bg-background px-4 py-2.5 text-sm font-medium text-text-secondary hover:bg-background-secondary hover:text-text-primary disabled:opacity-50"
|
|
210
|
+
>
|
|
211
|
+
Deny
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
onClick={() => handleConsent(true)}
|
|
215
|
+
disabled={isSubmitting}
|
|
216
|
+
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
217
|
+
>
|
|
218
|
+
{isSubmitting && <Loader2 size={16} className="animate-spin" />}
|
|
219
|
+
Authorize
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Footer */}
|
|
227
|
+
<p className="mt-6 text-center text-xs text-text-muted">
|
|
228
|
+
By authorizing, you allow this application to access your account
|
|
229
|
+
according to its terms of service and privacy policy.
|
|
230
|
+
</p>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|