@agent-relay/dashboard 2.0.80 → 2.0.82
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/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login Page - GitHub OAuth via Nango or Email/Password
|
|
3
|
+
*
|
|
4
|
+
* Key: Initialize Nango on page load, not on click.
|
|
5
|
+
* This avoids popup blockers by ensuring openConnectUI is synchronous.
|
|
6
|
+
* See: https://arveknudsen.com/posts/avoiding-popup-blocking-when-authing-with-google/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
import React, { useState, useEffect, useRef, Suspense } from 'react';
|
|
12
|
+
import { useSearchParams } from 'next/navigation';
|
|
13
|
+
import Nango from '@nangohq/frontend';
|
|
14
|
+
import { LogoIcon } from '../../components/Logo';
|
|
15
|
+
|
|
16
|
+
// Loading fallback for Suspense
|
|
17
|
+
function LoginLoading() {
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex flex-col items-center justify-center p-4">
|
|
20
|
+
<div className="relative z-10 w-full max-w-md">
|
|
21
|
+
<div className="flex flex-col items-center mb-8">
|
|
22
|
+
<LogoIcon size={48} withGlow={true} />
|
|
23
|
+
<h1 className="mt-4 text-2xl font-bold text-white">Agent Relay</h1>
|
|
24
|
+
<p className="mt-2 text-text-muted">Loading...</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type AuthMethod = 'github' | 'email';
|
|
32
|
+
|
|
33
|
+
// Main login content that uses useSearchParams
|
|
34
|
+
function LoginContent() {
|
|
35
|
+
const searchParams = useSearchParams();
|
|
36
|
+
const [authMethod, setAuthMethod] = useState<AuthMethod>('github');
|
|
37
|
+
const [isReady, setIsReady] = useState(false);
|
|
38
|
+
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
39
|
+
const [authStatus, setAuthStatus] = useState<string>('');
|
|
40
|
+
const [error, setError] = useState('');
|
|
41
|
+
|
|
42
|
+
// Email form state
|
|
43
|
+
const [email, setEmail] = useState('');
|
|
44
|
+
const [password, setPassword] = useState('');
|
|
45
|
+
|
|
46
|
+
// Get return URL from query params (used by cloud link flow)
|
|
47
|
+
const returnUrl = searchParams.get('return');
|
|
48
|
+
|
|
49
|
+
// Store Nango instance and session token - initialized on mount
|
|
50
|
+
const nangoRef = useRef<InstanceType<typeof Nango> | null>(null);
|
|
51
|
+
|
|
52
|
+
// Initialize Nango with session token on page load
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let mounted = true;
|
|
55
|
+
|
|
56
|
+
const init = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch('/api/auth/nango/login-session', {
|
|
59
|
+
credentials: 'include',
|
|
60
|
+
});
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
|
|
63
|
+
if (!mounted) return;
|
|
64
|
+
|
|
65
|
+
if (!response.ok || !data.sessionToken) {
|
|
66
|
+
// Don't set error - email login doesn't need Nango
|
|
67
|
+
setIsReady(true);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create Nango instance NOW, not on click
|
|
72
|
+
nangoRef.current = new Nango({ connectSessionToken: data.sessionToken });
|
|
73
|
+
setIsReady(true);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (mounted) {
|
|
76
|
+
console.error('Init error:', err);
|
|
77
|
+
// Still allow email login even if Nango fails
|
|
78
|
+
setIsReady(true);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
init();
|
|
84
|
+
return () => { mounted = false; };
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const checkAuthStatus = async (connectionId: string): Promise<{ ready: boolean; hasRepos?: boolean; needsEmail?: boolean }> => {
|
|
88
|
+
const response = await fetch(`/api/auth/nango/login-status/${connectionId}`, {
|
|
89
|
+
credentials: 'include',
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error('Auth status not ready');
|
|
93
|
+
}
|
|
94
|
+
return response.json();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleAuthSuccess = async (connectionId: string) => {
|
|
98
|
+
try {
|
|
99
|
+
setAuthStatus('Completing authentication...');
|
|
100
|
+
|
|
101
|
+
const pollStartTime = Date.now();
|
|
102
|
+
const maxPollTime = 30000;
|
|
103
|
+
const pollInterval = 1000;
|
|
104
|
+
|
|
105
|
+
const pollForAuth = async (): Promise<void> => {
|
|
106
|
+
const elapsed = Date.now() - pollStartTime;
|
|
107
|
+
|
|
108
|
+
if (elapsed > maxPollTime) {
|
|
109
|
+
throw new Error('Authentication timed out. Please try again.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const result = await checkAuthStatus(connectionId);
|
|
114
|
+
if (result && result.ready) {
|
|
115
|
+
// If user needs to provide email, redirect to complete-profile
|
|
116
|
+
if (result.needsEmail) {
|
|
117
|
+
window.location.href = '/complete-profile';
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Redirect to return URL if provided (e.g., cloud link flow),
|
|
121
|
+
// otherwise to connect-repos if no repos, or to app
|
|
122
|
+
if (returnUrl) {
|
|
123
|
+
window.location.href = returnUrl;
|
|
124
|
+
} else {
|
|
125
|
+
window.location.href = result.hasRepos ? '/app' : '/connect-repos';
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
131
|
+
return pollForAuth();
|
|
132
|
+
} catch {
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
134
|
+
return pollForAuth();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
await pollForAuth();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error('[AUTH] Authentication error:', err);
|
|
141
|
+
setError(err instanceof Error ? err.message : 'Authentication failed');
|
|
142
|
+
setIsAuthenticating(false);
|
|
143
|
+
setAuthStatus('');
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Use nango.auth() instead of openConnectUI to avoid popup blocker issues
|
|
148
|
+
const handleGitHubAuth = async () => {
|
|
149
|
+
if (!nangoRef.current) {
|
|
150
|
+
setError('GitHub login not available. Please use email login or refresh the page.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setIsAuthenticating(true);
|
|
155
|
+
setError('');
|
|
156
|
+
setAuthStatus('Connecting to GitHub...');
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const result = await nangoRef.current.auth('github');
|
|
160
|
+
if (result && 'connectionId' in result) {
|
|
161
|
+
await handleAuthSuccess(result.connectionId);
|
|
162
|
+
} else {
|
|
163
|
+
throw new Error('No connection ID returned');
|
|
164
|
+
}
|
|
165
|
+
} catch (err: unknown) {
|
|
166
|
+
const error = err as Error & { type?: string };
|
|
167
|
+
console.error('GitHub auth error:', error);
|
|
168
|
+
|
|
169
|
+
// Don't show error for user-cancelled auth
|
|
170
|
+
if (error.type === 'user_cancelled' || error.message?.includes('closed')) {
|
|
171
|
+
setIsAuthenticating(false);
|
|
172
|
+
setAuthStatus('');
|
|
173
|
+
// Re-initialize for next attempt
|
|
174
|
+
fetch('/api/auth/nango/login-session', { credentials: 'include' })
|
|
175
|
+
.then(res => res.json())
|
|
176
|
+
.then(data => {
|
|
177
|
+
if (data.sessionToken) {
|
|
178
|
+
nangoRef.current = new Nango({ connectSessionToken: data.sessionToken });
|
|
179
|
+
setIsReady(true);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setError(error.message || 'Authentication failed');
|
|
186
|
+
setIsAuthenticating(false);
|
|
187
|
+
setAuthStatus('');
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Handle email login
|
|
192
|
+
const handleEmailLogin = async (e: React.FormEvent) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
setError('');
|
|
195
|
+
setIsAuthenticating(true);
|
|
196
|
+
setAuthStatus('Signing in...');
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Get CSRF token first
|
|
200
|
+
const csrfResponse = await fetch('/api/auth/session', { credentials: 'include' });
|
|
201
|
+
const csrfToken = csrfResponse.headers.get('x-csrf-token');
|
|
202
|
+
|
|
203
|
+
const response = await fetch('/api/auth/email/login', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: {
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
...(csrfToken && { 'x-csrf-token': csrfToken }),
|
|
208
|
+
},
|
|
209
|
+
credentials: 'include',
|
|
210
|
+
body: JSON.stringify({ email, password }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const data = await response.json();
|
|
214
|
+
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
// If user has GitHub account, suggest that
|
|
217
|
+
if (data.code === 'GITHUB_ACCOUNT') {
|
|
218
|
+
setError(data.error);
|
|
219
|
+
setAuthMethod('github');
|
|
220
|
+
} else {
|
|
221
|
+
setError(data.error || 'Login failed');
|
|
222
|
+
}
|
|
223
|
+
setIsAuthenticating(false);
|
|
224
|
+
setAuthStatus('');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Success - redirect
|
|
229
|
+
setAuthStatus('Login successful! Redirecting...');
|
|
230
|
+
if (returnUrl) {
|
|
231
|
+
window.location.href = returnUrl;
|
|
232
|
+
} else {
|
|
233
|
+
window.location.href = '/app';
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error('Email login error:', err);
|
|
237
|
+
setError('Failed to connect. Please try again.');
|
|
238
|
+
setIsAuthenticating(false);
|
|
239
|
+
setAuthStatus('');
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const isLoading = !isReady || isAuthenticating;
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex flex-col items-center justify-center p-4">
|
|
247
|
+
{/* Background grid */}
|
|
248
|
+
<div className="fixed inset-0 opacity-10">
|
|
249
|
+
<div
|
|
250
|
+
className="absolute inset-0"
|
|
251
|
+
style={{
|
|
252
|
+
backgroundImage: `linear-gradient(rgba(0, 217, 255, 0.1) 1px, transparent 1px),
|
|
253
|
+
linear-gradient(90deg, rgba(0, 217, 255, 0.1) 1px, transparent 1px)`,
|
|
254
|
+
backgroundSize: '50px 50px',
|
|
255
|
+
}}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Content */}
|
|
260
|
+
<div className="relative z-10 w-full max-w-md">
|
|
261
|
+
{/* Logo */}
|
|
262
|
+
<div className="flex flex-col items-center mb-8">
|
|
263
|
+
<LogoIcon size={48} withGlow={true} />
|
|
264
|
+
<h1 className="mt-4 text-2xl font-bold text-white">Agent Relay</h1>
|
|
265
|
+
<p className="mt-2 text-text-muted">Sign in to continue</p>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Login Card */}
|
|
269
|
+
<div className="bg-bg-primary/80 backdrop-blur-sm border border-border-subtle rounded-2xl p-8 shadow-xl">
|
|
270
|
+
{/* Auth method tabs */}
|
|
271
|
+
<div className="flex mb-6 bg-bg-secondary/50 rounded-lg p-1">
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={() => setAuthMethod('github')}
|
|
275
|
+
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
|
|
276
|
+
authMethod === 'github'
|
|
277
|
+
? 'bg-bg-primary text-white shadow-sm'
|
|
278
|
+
: 'text-text-muted hover:text-white'
|
|
279
|
+
}`}
|
|
280
|
+
>
|
|
281
|
+
GitHub
|
|
282
|
+
</button>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={() => setAuthMethod('email')}
|
|
286
|
+
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
|
|
287
|
+
authMethod === 'email'
|
|
288
|
+
? 'bg-bg-primary text-white shadow-sm'
|
|
289
|
+
: 'text-text-muted hover:text-white'
|
|
290
|
+
}`}
|
|
291
|
+
>
|
|
292
|
+
Email
|
|
293
|
+
</button>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div>
|
|
297
|
+
{error && (
|
|
298
|
+
<div className="mb-4 p-3 bg-error/10 border border-error/20 rounded-lg">
|
|
299
|
+
<p className="text-error text-sm">{error}</p>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{authMethod === 'github' ? (
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
onClick={handleGitHubAuth}
|
|
307
|
+
disabled={isLoading}
|
|
308
|
+
className="w-full py-4 px-6 bg-[#24292e] hover:bg-[#2f363d] border border-[#444d56] rounded-xl text-white font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
309
|
+
>
|
|
310
|
+
{!isReady ? (
|
|
311
|
+
<>
|
|
312
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
313
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
314
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
315
|
+
</svg>
|
|
316
|
+
<span>Loading...</span>
|
|
317
|
+
</>
|
|
318
|
+
) : isAuthenticating ? (
|
|
319
|
+
<>
|
|
320
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
321
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
322
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
323
|
+
</svg>
|
|
324
|
+
<span>{authStatus || 'Connecting...'}</span>
|
|
325
|
+
</>
|
|
326
|
+
) : (
|
|
327
|
+
<>
|
|
328
|
+
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
329
|
+
<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" />
|
|
330
|
+
</svg>
|
|
331
|
+
<span>Continue with GitHub</span>
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</button>
|
|
335
|
+
) : (
|
|
336
|
+
<form onSubmit={handleEmailLogin} className="space-y-4">
|
|
337
|
+
<div>
|
|
338
|
+
<label htmlFor="email" className="block text-sm font-medium text-text-secondary mb-2">
|
|
339
|
+
Email
|
|
340
|
+
</label>
|
|
341
|
+
<input
|
|
342
|
+
type="email"
|
|
343
|
+
id="email"
|
|
344
|
+
value={email}
|
|
345
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
346
|
+
required
|
|
347
|
+
disabled={isAuthenticating}
|
|
348
|
+
className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
|
|
349
|
+
placeholder="you@example.com"
|
|
350
|
+
/>
|
|
351
|
+
</div>
|
|
352
|
+
<div>
|
|
353
|
+
<label htmlFor="password" className="block text-sm font-medium text-text-secondary mb-2">
|
|
354
|
+
Password
|
|
355
|
+
</label>
|
|
356
|
+
<input
|
|
357
|
+
type="password"
|
|
358
|
+
id="password"
|
|
359
|
+
value={password}
|
|
360
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
361
|
+
required
|
|
362
|
+
disabled={isAuthenticating}
|
|
363
|
+
className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
|
|
364
|
+
placeholder="Enter your password"
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
<button
|
|
368
|
+
type="submit"
|
|
369
|
+
disabled={isLoading || !email || !password}
|
|
370
|
+
className="w-full py-4 px-6 bg-accent-cyan hover:bg-accent-cyan/90 rounded-xl text-black font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
371
|
+
>
|
|
372
|
+
{isAuthenticating ? (
|
|
373
|
+
<>
|
|
374
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
375
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
376
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
377
|
+
</svg>
|
|
378
|
+
<span>{authStatus || 'Signing in...'}</span>
|
|
379
|
+
</>
|
|
380
|
+
) : (
|
|
381
|
+
<span>Sign in with Email</span>
|
|
382
|
+
)}
|
|
383
|
+
</button>
|
|
384
|
+
</form>
|
|
385
|
+
)}
|
|
386
|
+
|
|
387
|
+
<p className="mt-6 text-center text-text-muted text-sm">
|
|
388
|
+
By signing in, you agree to our{' '}
|
|
389
|
+
<a href="/terms" className="text-accent-cyan hover:underline">Terms of Service</a>
|
|
390
|
+
{' '}and{' '}
|
|
391
|
+
<a href="/privacy" className="text-accent-cyan hover:underline">Privacy Policy</a>
|
|
392
|
+
</p>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
{/* Sign up link */}
|
|
397
|
+
<div className="mt-6 text-center">
|
|
398
|
+
<p className="text-text-muted">
|
|
399
|
+
Don't have an account?{' '}
|
|
400
|
+
<a href="/signup" className="text-accent-cyan hover:underline font-medium">
|
|
401
|
+
Sign up
|
|
402
|
+
</a>
|
|
403
|
+
</p>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{/* Back to home */}
|
|
407
|
+
<div className="mt-4 text-center">
|
|
408
|
+
<a href="/" className="text-text-muted hover:text-white transition-colors text-sm">
|
|
409
|
+
Back to home
|
|
410
|
+
</a>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Export page wrapped in Suspense for static generation
|
|
418
|
+
export default function LoginPage() {
|
|
419
|
+
return (
|
|
420
|
+
<Suspense fallback={<LoginLoading />}>
|
|
421
|
+
<LoginContent />
|
|
422
|
+
</Suspense>
|
|
423
|
+
);
|
|
424
|
+
}
|