agent-relay 2.0.16 → 2.0.18
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/deploy/workspace/entrypoint.sh +35 -19
- package/deploy/workspace/git-credential-relay +82 -7
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
- package/{packages/dashboard/ui-dist/_next/static/chunks/app/app/page-9d6bc8729b429956.js → dist/dashboard/out/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
- package/dist/dashboard/out/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +1 -1
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/cloud/link.html +1 -1
- package/dist/dashboard/out/cloud/link.txt +1 -1
- package/dist/dashboard/out/complete-profile.html +5 -0
- package/dist/dashboard/out/complete-profile.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +1 -1
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +1 -1
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +1 -1
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +1 -1
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +1 -1
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +1 -1
- package/dist/dashboard/out/providers/setup/cursor.html +1 -1
- package/dist/dashboard/out/providers/setup/cursor.txt +1 -1
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +2 -2
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +2 -2
- package/dist/src/cli/index.js +3 -1
- package/package.json +22 -21
- package/packages/api-types/package.json +1 -1
- package/packages/bridge/package.json +8 -8
- package/packages/cloud/dist/api/auth.js +2 -0
- package/packages/cloud/dist/api/billing.js +4 -4
- package/packages/cloud/dist/api/email-auth.d.ts +11 -0
- package/packages/cloud/dist/api/email-auth.js +347 -0
- package/packages/cloud/dist/api/nango-auth.js +72 -5
- package/packages/cloud/dist/db/drizzle.d.ts +35 -1
- package/packages/cloud/dist/db/drizzle.js +136 -0
- package/packages/cloud/dist/db/index.d.ts +5 -4
- package/packages/cloud/dist/db/index.js +5 -3
- package/packages/cloud/dist/db/schema.d.ts +246 -2
- package/packages/cloud/dist/db/schema.js +39 -3
- package/packages/cloud/dist/provisioner/index.js +5 -1
- package/packages/cloud/dist/server.js +134 -24
- package/packages/cloud/dist/services/nango.d.ts +18 -0
- package/packages/cloud/dist/services/nango.js +32 -0
- package/packages/cloud/package.json +6 -6
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +1 -1
- package/packages/daemon/package.json +12 -12
- package/packages/dashboard/dist/server.js +36 -7
- package/packages/dashboard/package.json +13 -13
- package/packages/dashboard/ui/app/complete-profile/page.tsx +204 -0
- package/packages/dashboard/ui/app/login/page.tsx +182 -38
- package/packages/dashboard/ui/app/signup/page.tsx +244 -54
- package/packages/dashboard/ui/lib/cloudApi.ts +1 -0
- package/packages/dashboard/ui/react-components/App.tsx +1 -1
- package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +10 -0
- package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +160 -3
- package/packages/dashboard/ui-dist/404.html +1 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
- package/{dist/dashboard/out/_next/static/chunks/app/app/page-9d6bc8729b429956.js → packages/dashboard/ui-dist/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
- package/packages/dashboard/ui-dist/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.txt +1 -1
- package/packages/dashboard/ui-dist/app.html +1 -1
- package/packages/dashboard/ui-dist/app.txt +2 -2
- package/packages/dashboard/ui-dist/cloud/link.html +1 -1
- package/packages/dashboard/ui-dist/cloud/link.txt +1 -1
- package/packages/dashboard/ui-dist/complete-profile.html +5 -0
- package/packages/dashboard/ui-dist/complete-profile.txt +7 -0
- package/packages/dashboard/ui-dist/connect-repos.html +1 -1
- package/packages/dashboard/ui-dist/connect-repos.txt +1 -1
- package/packages/dashboard/ui-dist/history.html +1 -1
- package/packages/dashboard/ui-dist/history.txt +1 -1
- package/packages/dashboard/ui-dist/index.html +1 -1
- package/packages/dashboard/ui-dist/index.txt +2 -2
- package/packages/dashboard/ui-dist/login.html +2 -2
- package/packages/dashboard/ui-dist/login.txt +2 -2
- package/packages/dashboard/ui-dist/metrics.html +1 -1
- package/packages/dashboard/ui-dist/metrics.txt +1 -1
- package/packages/dashboard/ui-dist/pricing.html +2 -2
- package/packages/dashboard/ui-dist/pricing.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.txt +1 -1
- package/packages/dashboard/ui-dist/providers.html +1 -1
- package/packages/dashboard/ui-dist/providers.txt +2 -2
- package/packages/dashboard/ui-dist/signup.html +2 -2
- package/packages/dashboard/ui-dist/signup.txt +2 -2
- package/packages/dashboard-server/dist/server.js +36 -7
- package/packages/dashboard-server/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +1 -1
- package/packages/wrapper/dist/relay-pty-orchestrator.js +17 -3
- package/packages/wrapper/package.json +6 -6
- package/relay-snippets/agent-policy-snippet.md +40 -0
- package/relay-snippets/agent-relay-protocol.md +101 -0
- package/relay-snippets/agent-relay-snippet.md +177 -0
- package/SESSION_HANDOFF.md +0 -67
- package/dist/dashboard/out/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
- package/test-push.txt +0 -1
- /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Signup Page - GitHub OAuth via Nango
|
|
2
|
+
* Signup Page - GitHub OAuth via Nango or Email/Password
|
|
3
3
|
*
|
|
4
4
|
* Key: Initialize Nango on page load, not on click.
|
|
5
5
|
* This avoids popup blockers by ensuring openConnectUI is synchronous.
|
|
@@ -11,7 +11,10 @@ import React, { useState, useEffect, useRef } from 'react';
|
|
|
11
11
|
import Nango from '@nangohq/frontend';
|
|
12
12
|
import { LogoIcon } from '../../react-components/Logo';
|
|
13
13
|
|
|
14
|
+
type AuthMethod = 'github' | 'email';
|
|
15
|
+
|
|
14
16
|
export default function SignupPage() {
|
|
17
|
+
const [authMethod, setAuthMethod] = useState<AuthMethod>('github');
|
|
15
18
|
const [isReady, setIsReady] = useState(false);
|
|
16
19
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
17
20
|
const [authStatus, setAuthStatus] = useState<string>('');
|
|
@@ -19,6 +22,12 @@ export default function SignupPage() {
|
|
|
19
22
|
const [redirectTarget, setRedirectTarget] = useState<string>('/app');
|
|
20
23
|
const [showSuccess, setShowSuccess] = useState(false);
|
|
21
24
|
|
|
25
|
+
// Email signup form state
|
|
26
|
+
const [email, setEmail] = useState('');
|
|
27
|
+
const [password, setPassword] = useState('');
|
|
28
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
29
|
+
const [displayName, setDisplayName] = useState('');
|
|
30
|
+
|
|
22
31
|
// Store Nango instance - initialized on mount
|
|
23
32
|
const nangoRef = useRef<InstanceType<typeof Nango> | null>(null);
|
|
24
33
|
|
|
@@ -49,7 +58,8 @@ export default function SignupPage() {
|
|
|
49
58
|
if (!mounted) return;
|
|
50
59
|
|
|
51
60
|
if (!response.ok || !data.sessionToken) {
|
|
52
|
-
|
|
61
|
+
// Don't set error - email signup doesn't need Nango
|
|
62
|
+
setIsReady(true);
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
55
65
|
|
|
@@ -59,7 +69,8 @@ export default function SignupPage() {
|
|
|
59
69
|
} catch (err) {
|
|
60
70
|
if (mounted) {
|
|
61
71
|
console.error('Init error:', err);
|
|
62
|
-
|
|
72
|
+
// Still allow email signup even if Nango fails
|
|
73
|
+
setIsReady(true);
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
};
|
|
@@ -98,7 +109,7 @@ export default function SignupPage() {
|
|
|
98
109
|
}
|
|
99
110
|
};
|
|
100
111
|
|
|
101
|
-
const checkAuthStatus = async (connectionId: string): Promise<{ ready: boolean }> => {
|
|
112
|
+
const checkAuthStatus = async (connectionId: string): Promise<{ ready: boolean; needsEmail?: boolean }> => {
|
|
102
113
|
const response = await fetch(`/api/auth/nango/login-status/${connectionId}`, {
|
|
103
114
|
credentials: 'include',
|
|
104
115
|
});
|
|
@@ -126,6 +137,11 @@ export default function SignupPage() {
|
|
|
126
137
|
try {
|
|
127
138
|
const result = await checkAuthStatus(connectionId);
|
|
128
139
|
if (result && result.ready) {
|
|
140
|
+
// If user needs to provide email, redirect to complete-profile
|
|
141
|
+
if (result.needsEmail) {
|
|
142
|
+
window.location.href = '/complete-profile';
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
129
145
|
await handlePostAuthRedirect();
|
|
130
146
|
return;
|
|
131
147
|
}
|
|
@@ -150,7 +166,7 @@ export default function SignupPage() {
|
|
|
150
166
|
// Use nango.auth() instead of openConnectUI to avoid popup blocker issues
|
|
151
167
|
const handleGitHubAuth = async () => {
|
|
152
168
|
if (!nangoRef.current) {
|
|
153
|
-
setError('
|
|
169
|
+
setError('GitHub signup not available. Please use email signup or refresh the page.');
|
|
154
170
|
return;
|
|
155
171
|
}
|
|
156
172
|
|
|
@@ -191,6 +207,70 @@ export default function SignupPage() {
|
|
|
191
207
|
}
|
|
192
208
|
};
|
|
193
209
|
|
|
210
|
+
// Handle email signup
|
|
211
|
+
const handleEmailSignup = async (e: React.FormEvent) => {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
setError('');
|
|
214
|
+
|
|
215
|
+
// Validate passwords match
|
|
216
|
+
if (password !== confirmPassword) {
|
|
217
|
+
setError('Passwords do not match');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Validate password length
|
|
222
|
+
if (password.length < 8) {
|
|
223
|
+
setError('Password must be at least 8 characters long');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setIsAuthenticating(true);
|
|
228
|
+
setAuthStatus('Creating your account...');
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// Get CSRF token first
|
|
232
|
+
const csrfResponse = await fetch('/api/auth/session', { credentials: 'include' });
|
|
233
|
+
const csrfToken = csrfResponse.headers.get('x-csrf-token');
|
|
234
|
+
|
|
235
|
+
const response = await fetch('/api/auth/email/signup', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
'Content-Type': 'application/json',
|
|
239
|
+
...(csrfToken && { 'x-csrf-token': csrfToken }),
|
|
240
|
+
},
|
|
241
|
+
credentials: 'include',
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
email,
|
|
244
|
+
password,
|
|
245
|
+
displayName: displayName || undefined,
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
setError(data.error || 'Signup failed');
|
|
253
|
+
setIsAuthenticating(false);
|
|
254
|
+
setAuthStatus('');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Success - redirect to app
|
|
259
|
+
setAuthStatus('Account created! Redirecting...');
|
|
260
|
+
setShowSuccess(true);
|
|
261
|
+
setRedirectTarget('/app');
|
|
262
|
+
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
window.location.href = '/app';
|
|
265
|
+
}, 1500);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error('Email signup error:', err);
|
|
268
|
+
setError('Failed to connect. Please try again.');
|
|
269
|
+
setIsAuthenticating(false);
|
|
270
|
+
setAuthStatus('');
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
194
274
|
const isLoading = !isReady || isAuthenticating;
|
|
195
275
|
|
|
196
276
|
return (
|
|
@@ -249,7 +329,7 @@ export default function SignupPage() {
|
|
|
249
329
|
</svg>
|
|
250
330
|
</div>
|
|
251
331
|
<h2 className="text-xl font-semibold text-white mb-2">Creating Account</h2>
|
|
252
|
-
<p className="text-text-muted">{authStatus || '
|
|
332
|
+
<p className="text-text-muted">{authStatus || 'Setting things up...'}</p>
|
|
253
333
|
</div>
|
|
254
334
|
) : (
|
|
255
335
|
<div>
|
|
@@ -259,57 +339,167 @@ export default function SignupPage() {
|
|
|
259
339
|
</div>
|
|
260
340
|
)}
|
|
261
341
|
|
|
262
|
-
{/*
|
|
263
|
-
<div className="mb-6
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
342
|
+
{/* Auth method tabs */}
|
|
343
|
+
<div className="flex mb-6 bg-bg-secondary/50 rounded-lg p-1">
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={() => setAuthMethod('github')}
|
|
347
|
+
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
|
|
348
|
+
authMethod === 'github'
|
|
349
|
+
? 'bg-bg-primary text-white shadow-sm'
|
|
350
|
+
: 'text-text-muted hover:text-white'
|
|
351
|
+
}`}
|
|
352
|
+
>
|
|
353
|
+
GitHub
|
|
354
|
+
</button>
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
onClick={() => setAuthMethod('email')}
|
|
358
|
+
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
|
|
359
|
+
authMethod === 'email'
|
|
360
|
+
? 'bg-bg-primary text-white shadow-sm'
|
|
361
|
+
: 'text-text-muted hover:text-white'
|
|
362
|
+
}`}
|
|
363
|
+
>
|
|
364
|
+
Email
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
{authMethod === 'github' ? (
|
|
369
|
+
<>
|
|
370
|
+
{/* Features list */}
|
|
371
|
+
<div className="mb-6 space-y-3">
|
|
372
|
+
<div className="flex items-center gap-3 text-sm text-text-secondary">
|
|
373
|
+
<div className="w-8 h-8 rounded-lg bg-accent-cyan/10 flex items-center justify-center flex-shrink-0">
|
|
374
|
+
<svg className="w-4 h-4 text-accent-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
375
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
376
|
+
</svg>
|
|
377
|
+
</div>
|
|
378
|
+
<span>Deploy AI agents in seconds</span>
|
|
379
|
+
</div>
|
|
380
|
+
<div className="flex items-center gap-3 text-sm text-text-secondary">
|
|
381
|
+
<div className="w-8 h-8 rounded-lg bg-[#00ffc8]/10 flex items-center justify-center flex-shrink-0">
|
|
382
|
+
<svg className="w-4 h-4 text-[#00ffc8]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
383
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
384
|
+
</svg>
|
|
385
|
+
</div>
|
|
386
|
+
<span>Real-time agent collaboration</span>
|
|
387
|
+
</div>
|
|
388
|
+
<div className="flex items-center gap-3 text-sm text-text-secondary">
|
|
389
|
+
<div className="w-8 h-8 rounded-lg bg-[#0891b2]/10 flex items-center justify-center flex-shrink-0">
|
|
390
|
+
<svg className="w-4 h-4 text-[#0891b2]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
391
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
392
|
+
</svg>
|
|
393
|
+
</div>
|
|
394
|
+
<span>Secure credential management</span>
|
|
395
|
+
</div>
|
|
269
396
|
</div>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
397
|
+
|
|
398
|
+
<button
|
|
399
|
+
type="button"
|
|
400
|
+
onClick={handleGitHubAuth}
|
|
401
|
+
disabled={isLoading}
|
|
402
|
+
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"
|
|
403
|
+
>
|
|
404
|
+
{!isReady ? (
|
|
405
|
+
<>
|
|
406
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
407
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
408
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
409
|
+
</svg>
|
|
410
|
+
<span>Loading...</span>
|
|
411
|
+
</>
|
|
412
|
+
) : (
|
|
413
|
+
<>
|
|
414
|
+
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
415
|
+
<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" />
|
|
416
|
+
</svg>
|
|
417
|
+
<span>Sign up with GitHub</span>
|
|
418
|
+
</>
|
|
419
|
+
)}
|
|
420
|
+
</button>
|
|
421
|
+
</>
|
|
422
|
+
) : (
|
|
423
|
+
<form onSubmit={handleEmailSignup} className="space-y-4">
|
|
424
|
+
<div>
|
|
425
|
+
<label htmlFor="displayName" className="block text-sm font-medium text-text-secondary mb-2">
|
|
426
|
+
Name <span className="text-text-muted">(optional)</span>
|
|
427
|
+
</label>
|
|
428
|
+
<input
|
|
429
|
+
type="text"
|
|
430
|
+
id="displayName"
|
|
431
|
+
value={displayName}
|
|
432
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
433
|
+
disabled={isAuthenticating}
|
|
434
|
+
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"
|
|
435
|
+
placeholder="Your name"
|
|
436
|
+
/>
|
|
277
437
|
</div>
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
438
|
+
<div>
|
|
439
|
+
<label htmlFor="email" className="block text-sm font-medium text-text-secondary mb-2">
|
|
440
|
+
Email
|
|
441
|
+
</label>
|
|
442
|
+
<input
|
|
443
|
+
type="email"
|
|
444
|
+
id="email"
|
|
445
|
+
value={email}
|
|
446
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
447
|
+
required
|
|
448
|
+
disabled={isAuthenticating}
|
|
449
|
+
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"
|
|
450
|
+
placeholder="you@example.com"
|
|
451
|
+
/>
|
|
285
452
|
</div>
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
453
|
+
<div>
|
|
454
|
+
<label htmlFor="password" className="block text-sm font-medium text-text-secondary mb-2">
|
|
455
|
+
Password
|
|
456
|
+
</label>
|
|
457
|
+
<input
|
|
458
|
+
type="password"
|
|
459
|
+
id="password"
|
|
460
|
+
value={password}
|
|
461
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
462
|
+
required
|
|
463
|
+
minLength={8}
|
|
464
|
+
disabled={isAuthenticating}
|
|
465
|
+
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"
|
|
466
|
+
placeholder="At least 8 characters"
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
469
|
+
<div>
|
|
470
|
+
<label htmlFor="confirmPassword" className="block text-sm font-medium text-text-secondary mb-2">
|
|
471
|
+
Confirm Password
|
|
472
|
+
</label>
|
|
473
|
+
<input
|
|
474
|
+
type="password"
|
|
475
|
+
id="confirmPassword"
|
|
476
|
+
value={confirmPassword}
|
|
477
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
478
|
+
required
|
|
479
|
+
disabled={isAuthenticating}
|
|
480
|
+
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"
|
|
481
|
+
placeholder="Confirm your password"
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
484
|
+
<button
|
|
485
|
+
type="submit"
|
|
486
|
+
disabled={isLoading || !email || !password || !confirmPassword}
|
|
487
|
+
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"
|
|
488
|
+
>
|
|
489
|
+
{isAuthenticating ? (
|
|
490
|
+
<>
|
|
491
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
492
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
493
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
494
|
+
</svg>
|
|
495
|
+
<span>{authStatus || 'Creating account...'}</span>
|
|
496
|
+
</>
|
|
497
|
+
) : (
|
|
498
|
+
<span>Create Account</span>
|
|
499
|
+
)}
|
|
500
|
+
</button>
|
|
501
|
+
</form>
|
|
502
|
+
)}
|
|
313
503
|
|
|
314
504
|
<p className="mt-6 text-center text-text-muted text-sm">
|
|
315
505
|
By signing up, you agree to our{' '}
|
|
@@ -221,7 +221,7 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) {
|
|
|
221
221
|
// Derive current user from cloud session (falls back to undefined in non-cloud mode)
|
|
222
222
|
const currentUser: CurrentUser | undefined = cloudSession?.user
|
|
223
223
|
? {
|
|
224
|
-
displayName: cloudSession.user.githubUsername,
|
|
224
|
+
displayName: cloudSession.user.githubUsername || cloudSession.user.displayName || '',
|
|
225
225
|
avatarUrl: cloudSession.user.avatarUrl,
|
|
226
226
|
}
|
|
227
227
|
: undefined;
|
|
@@ -65,6 +65,7 @@ export function ProviderAuthFlow({
|
|
|
65
65
|
const popupOpenedRef = useRef(false);
|
|
66
66
|
const pollingRef = useRef(false);
|
|
67
67
|
const cliPollingRef = useRef(false);
|
|
68
|
+
const completingRef = useRef(false); // Prevent double-calling handleComplete
|
|
68
69
|
|
|
69
70
|
const backendProviderId = PROVIDER_ID_MAP[provider.id] || provider.id;
|
|
70
71
|
|
|
@@ -320,6 +321,14 @@ export function ProviderAuthFlow({
|
|
|
320
321
|
const targetSessionId = sid || sessionId;
|
|
321
322
|
if (!targetSessionId) return;
|
|
322
323
|
|
|
324
|
+
// Prevent double-calling (both CLI polling and status polling may detect success)
|
|
325
|
+
if (completingRef.current) return;
|
|
326
|
+
completingRef.current = true;
|
|
327
|
+
|
|
328
|
+
// Stop both polling loops
|
|
329
|
+
pollingRef.current = false;
|
|
330
|
+
cliPollingRef.current = false;
|
|
331
|
+
|
|
323
332
|
setStatus('submitting');
|
|
324
333
|
setErrorMessage(null);
|
|
325
334
|
|
|
@@ -343,6 +352,7 @@ export function ProviderAuthFlow({
|
|
|
343
352
|
// Brief delay to show success message before parent unmounts component
|
|
344
353
|
setTimeout(() => onSuccess(), 1500);
|
|
345
354
|
} catch (err) {
|
|
355
|
+
completingRef.current = false; // Allow retry on error
|
|
346
356
|
const msg = err instanceof Error ? err.message : 'Failed to complete authentication';
|
|
347
357
|
setErrorMessage(msg);
|
|
348
358
|
setStatus('error');
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - POST /api/workspaces/quick - Create workspace for a repo
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
12
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
13
|
+
import Nango from '@nangohq/frontend';
|
|
13
14
|
|
|
14
15
|
interface AccessibleRepo {
|
|
15
16
|
id: number;
|
|
@@ -69,10 +70,17 @@ export function RepoAccessPanel({
|
|
|
69
70
|
const [repos, setRepos] = useState<AccessibleRepo[]>([]);
|
|
70
71
|
const [loadingState, setLoadingState] = useState<LoadingState>('idle');
|
|
71
72
|
const [error, setError] = useState<string | null>(null);
|
|
73
|
+
const [isGitHubNotConnected, setIsGitHubNotConnected] = useState(false);
|
|
72
74
|
const [creatingWorkspace, setCreatingWorkspace] = useState<string | null>(null);
|
|
73
75
|
const [searchQuery, setSearchQuery] = useState('');
|
|
74
76
|
const [filterType, setFilterType] = useState<'all' | 'with-workspace' | 'without-workspace'>('all');
|
|
75
77
|
|
|
78
|
+
// GitHub OAuth state
|
|
79
|
+
const nangoRef = useRef<InstanceType<typeof Nango> | null>(null);
|
|
80
|
+
const [isNangoReady, setIsNangoReady] = useState(false);
|
|
81
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
82
|
+
const [connectError, setConnectError] = useState<string | null>(null);
|
|
83
|
+
|
|
76
84
|
// Create a map of repo full names to workspace IDs for quick lookup
|
|
77
85
|
const repoToWorkspace = new Map<string, Workspace>();
|
|
78
86
|
workspaces.forEach(ws => {
|
|
@@ -85,6 +93,7 @@ export function RepoAccessPanel({
|
|
|
85
93
|
const fetchRepos = useCallback(async () => {
|
|
86
94
|
setLoadingState('loading');
|
|
87
95
|
setError(null);
|
|
96
|
+
setIsGitHubNotConnected(false);
|
|
88
97
|
|
|
89
98
|
try {
|
|
90
99
|
const response = await fetch('/api/repos/accessible?perPage=100', {
|
|
@@ -94,7 +103,8 @@ export function RepoAccessPanel({
|
|
|
94
103
|
if (!response.ok) {
|
|
95
104
|
const data = await response.json();
|
|
96
105
|
if (data.code === 'NANGO_NOT_CONNECTED') {
|
|
97
|
-
|
|
106
|
+
setIsGitHubNotConnected(true);
|
|
107
|
+
throw new Error('GitHub not connected. Connect your GitHub account to see your repositories.');
|
|
98
108
|
}
|
|
99
109
|
throw new Error(data.error || 'Failed to fetch repositories');
|
|
100
110
|
}
|
|
@@ -113,6 +123,98 @@ export function RepoAccessPanel({
|
|
|
113
123
|
fetchRepos();
|
|
114
124
|
}, [fetchRepos]);
|
|
115
125
|
|
|
126
|
+
// Initialize Nango when GitHub is not connected
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!isGitHubNotConnected) return;
|
|
129
|
+
|
|
130
|
+
let mounted = true;
|
|
131
|
+
|
|
132
|
+
const initNango = async () => {
|
|
133
|
+
try {
|
|
134
|
+
// Get Nango session token for GitHub login
|
|
135
|
+
const response = await fetch('/api/auth/nango/login-session', {
|
|
136
|
+
credentials: 'include',
|
|
137
|
+
});
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
|
|
140
|
+
if (!mounted) return;
|
|
141
|
+
|
|
142
|
+
if (response.ok && data.sessionToken) {
|
|
143
|
+
nangoRef.current = new Nango({ connectSessionToken: data.sessionToken });
|
|
144
|
+
setIsNangoReady(true);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('Failed to initialize Nango:', err);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
initNango();
|
|
152
|
+
return () => { mounted = false; };
|
|
153
|
+
}, [isGitHubNotConnected]);
|
|
154
|
+
|
|
155
|
+
// Handle GitHub OAuth connection
|
|
156
|
+
const handleConnectGitHub = async () => {
|
|
157
|
+
if (!nangoRef.current) {
|
|
158
|
+
setConnectError('GitHub connection not available. Please refresh the page.');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setIsConnecting(true);
|
|
163
|
+
setConnectError(null);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const result = await nangoRef.current.auth('github');
|
|
167
|
+
if (result && 'connectionId' in result) {
|
|
168
|
+
// Poll for auth completion
|
|
169
|
+
const pollForAuth = async (attempts = 0): Promise<void> => {
|
|
170
|
+
if (attempts > 30) {
|
|
171
|
+
throw new Error('Authentication timed out. Please try again.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const statusRes = await fetch(`/api/auth/nango/login-status/${result.connectionId}`, {
|
|
175
|
+
credentials: 'include',
|
|
176
|
+
});
|
|
177
|
+
const statusData = await statusRes.json();
|
|
178
|
+
|
|
179
|
+
if (statusData.ready) {
|
|
180
|
+
// Auth complete, refresh repos
|
|
181
|
+
setIsConnecting(false);
|
|
182
|
+
setIsGitHubNotConnected(false);
|
|
183
|
+
fetchRepos();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
188
|
+
return pollForAuth(attempts + 1);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
await pollForAuth();
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error('No connection ID returned');
|
|
194
|
+
}
|
|
195
|
+
} catch (err: unknown) {
|
|
196
|
+
const error = err as Error & { type?: string };
|
|
197
|
+
console.error('GitHub auth error:', error);
|
|
198
|
+
|
|
199
|
+
// Don't show error for user-cancelled auth
|
|
200
|
+
if (error.type === 'user_cancelled' || error.message?.includes('closed')) {
|
|
201
|
+
setIsConnecting(false);
|
|
202
|
+
// Re-initialize Nango for next attempt
|
|
203
|
+
fetch('/api/auth/nango/login-session', { credentials: 'include' })
|
|
204
|
+
.then(res => res.json())
|
|
205
|
+
.then(data => {
|
|
206
|
+
if (data.sessionToken) {
|
|
207
|
+
nangoRef.current = new Nango({ connectSessionToken: data.sessionToken });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setConnectError(error.message || 'Failed to connect GitHub');
|
|
214
|
+
setIsConnecting(false);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
116
218
|
// Create workspace for a repo
|
|
117
219
|
const handleCreateWorkspace = useCallback(async (repoFullName: string) => {
|
|
118
220
|
setCreatingWorkspace(repoFullName);
|
|
@@ -176,8 +278,55 @@ export function RepoAccessPanel({
|
|
|
176
278
|
);
|
|
177
279
|
}
|
|
178
280
|
|
|
179
|
-
// Error state
|
|
281
|
+
// Error state - special handling for GitHub not connected
|
|
180
282
|
if (loadingState === 'error') {
|
|
283
|
+
if (isGitHubNotConnected) {
|
|
284
|
+
return (
|
|
285
|
+
<div className={`p-6 ${className}`}>
|
|
286
|
+
<div className="bg-bg-tertiary border border-border-subtle rounded-xl p-8 text-center">
|
|
287
|
+
<div className="w-16 h-16 mx-auto mb-4 bg-bg-hover rounded-full flex items-center justify-center">
|
|
288
|
+
<GitHubIcon className="w-8 h-8 text-text-muted" />
|
|
289
|
+
</div>
|
|
290
|
+
<h3 className="text-lg font-semibold text-text-primary mb-2">Connect GitHub</h3>
|
|
291
|
+
<p className="text-text-muted mb-6 max-w-md mx-auto">
|
|
292
|
+
Connect your GitHub account to see your repositories and enable agent access to your code.
|
|
293
|
+
</p>
|
|
294
|
+
{connectError && (
|
|
295
|
+
<p className="text-error text-sm mb-4">{connectError}</p>
|
|
296
|
+
)}
|
|
297
|
+
<button
|
|
298
|
+
onClick={handleConnectGitHub}
|
|
299
|
+
disabled={!isNangoReady || isConnecting}
|
|
300
|
+
className="px-6 py-3 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-medium rounded-lg hover:shadow-glow-cyan transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
301
|
+
>
|
|
302
|
+
{isConnecting ? (
|
|
303
|
+
<span className="flex items-center gap-2">
|
|
304
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
305
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
306
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
307
|
+
</svg>
|
|
308
|
+
Connecting...
|
|
309
|
+
</span>
|
|
310
|
+
) : !isNangoReady ? (
|
|
311
|
+
<span className="flex items-center gap-2">
|
|
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
|
+
Loading...
|
|
317
|
+
</span>
|
|
318
|
+
) : (
|
|
319
|
+
<span className="flex items-center gap-2">
|
|
320
|
+
<GitHubIcon className="w-5 h-5" />
|
|
321
|
+
Connect GitHub Account
|
|
322
|
+
</span>
|
|
323
|
+
)}
|
|
324
|
+
</button>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
181
330
|
return (
|
|
182
331
|
<div className={`p-6 ${className}`}>
|
|
183
332
|
<div className="bg-error/10 border border-error/20 rounded-xl p-4 text-center">
|
|
@@ -390,3 +539,11 @@ function RepoIcon({ className = '' }: { className?: string }) {
|
|
|
390
539
|
</svg>
|
|
391
540
|
);
|
|
392
541
|
}
|
|
542
|
+
|
|
543
|
+
function GitHubIcon({ className = '' }: { className?: string }) {
|
|
544
|
+
return (
|
|
545
|
+
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
546
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
547
|
+
</svg>
|
|
548
|
+
);
|
|
549
|
+
}
|