Skip to content

Commit e6c867a

Browse files
committed
feat: step 1 of vercel integration
1 parent 0b69acd commit e6c867a

File tree

6 files changed

+4181
-4
lines changed

6 files changed

+4181
-4
lines changed

apps/dashboard/app/(auth)/login/page.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
SpinnerIcon,
1111
} from '@phosphor-icons/react';
1212
import Link from 'next/link';
13-
import { useRouter } from 'next/navigation';
13+
import { useRouter, useSearchParams } from 'next/navigation';
1414
import { Suspense, useEffect, useState } from 'react';
1515
import { toast } from 'sonner';
1616
import { Button } from '@/components/ui/button';
@@ -20,12 +20,15 @@ import { Separator } from '@/components/ui/separator';
2020

2121
function LoginPage() {
2222
const router = useRouter();
23+
const searchParams = useSearchParams();
2324
const [isLoading, setIsLoading] = useState(false);
2425
const [email, setEmail] = useState('');
2526
const [password, setPassword] = useState('');
2627
const [showPassword, setShowPassword] = useState(false);
2728
const [lastUsed, setLastUsed] = useState<string | null>(null);
2829

30+
const callbackUrl = searchParams.get('callback') || '/websites';
31+
2932
useEffect(() => {
3033
setLastUsed(localStorage.getItem('lastUsedLogin'));
3134
}, []);
@@ -34,7 +37,7 @@ function LoginPage() {
3437
setIsLoading(true);
3538
signIn.social({
3639
provider: 'google',
37-
callbackURL: '/websites',
40+
callbackURL: callbackUrl,
3841
newUserCallbackURL: '/onboarding',
3942
fetchOptions: {
4043
onSuccess: () => {
@@ -52,7 +55,7 @@ function LoginPage() {
5255
setIsLoading(true);
5356
signIn.social({
5457
provider: 'github',
55-
callbackURL: '/websites',
58+
callbackURL: callbackUrl,
5659
newUserCallbackURL: '/onboarding',
5760
fetchOptions: {
5861
onSuccess: () => {
@@ -78,7 +81,7 @@ function LoginPage() {
7881
await signIn.email({
7982
email,
8083
password,
81-
callbackURL: '/websites',
84+
callbackURL: callbackUrl,
8285
fetchOptions: {
8386
onSuccess: () => {
8487
localStorage.setItem('lastUsedLogin', 'email');
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { auth } from '@databuddy/auth';
2+
import { headers } from 'next/headers';
3+
import { redirect } from 'next/navigation';
4+
5+
export default async function VercelCallbackLayout({
6+
children,
7+
}: {
8+
children: React.ReactNode;
9+
}) {
10+
const session = await auth.api.getSession({ headers: await headers() });
11+
12+
// If user is not logged in, redirect to login with callback URL
13+
if (!session) {
14+
const currentUrl = new URL(
15+
'/integrations/vercel/callback',
16+
'https://app.databuddy.cc'
17+
);
18+
// Preserve all query parameters from the original callback
19+
const searchParams = new URLSearchParams();
20+
21+
// Get the current request URL to preserve query params
22+
const headersList = await headers();
23+
const fullUrl =
24+
headersList.get('x-url') || headersList.get('referer') || '';
25+
26+
if (fullUrl) {
27+
try {
28+
const url = new URL(fullUrl);
29+
url.searchParams.forEach((value, key) => {
30+
searchParams.set(key, value);
31+
});
32+
} catch (e) {
33+
// Fallback if URL parsing fails
34+
}
35+
}
36+
37+
const callbackUrl = `/integrations/vercel/callback?${searchParams.toString()}`;
38+
const loginUrl = `/login?callback=${encodeURIComponent(callbackUrl)}`;
39+
40+
redirect(loginUrl);
41+
}
42+
43+
return <>{children}</>;
44+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
'use client';
2+
3+
import { authClient } from '@databuddy/auth/client';
4+
import { SpinnerIcon } from '@phosphor-icons/react';
5+
import { useRouter, useSearchParams } from 'next/navigation';
6+
import { Suspense, useEffect, useState } from 'react';
7+
import { toast } from 'sonner';
8+
9+
function VercelCallbackContent() {
10+
const router = useRouter();
11+
const searchParams = useSearchParams();
12+
const [isProcessing, setIsProcessing] = useState(true);
13+
const [integrationData, setIntegrationData] = useState<{
14+
configurationId?: string;
15+
teamId?: string;
16+
next?: string;
17+
code?: string;
18+
} | null>(null);
19+
const [error, setError] = useState<string | null>(null);
20+
21+
useEffect(() => {
22+
const code = searchParams.get('code');
23+
const configurationId = searchParams.get('configurationId');
24+
const teamId = searchParams.get('teamId');
25+
const next = searchParams.get('next');
26+
27+
if (!code) {
28+
setError('Authorization code not found');
29+
setIsProcessing(false);
30+
return;
31+
}
32+
33+
setIntegrationData({
34+
code,
35+
configurationId: configurationId || undefined,
36+
teamId: teamId || undefined,
37+
next: next || undefined,
38+
});
39+
setIsProcessing(false);
40+
}, [searchParams]);
41+
42+
const handleConfirmIntegration = async () => {
43+
if (!integrationData?.code) {
44+
return;
45+
}
46+
47+
setIsProcessing(true);
48+
try {
49+
const { data, error } = await authClient.oauth2.link({
50+
providerId: 'vercel',
51+
callbackURL: '/websites',
52+
});
53+
54+
if (error) {
55+
setError(
56+
`Failed to connect Vercel account: ${error.message || 'Unknown error'}`
57+
);
58+
return;
59+
}
60+
61+
if (integrationData.configurationId || integrationData.teamId) {
62+
// TODO: Store these details in the database
63+
}
64+
65+
toast.success('Vercel account connected successfully!');
66+
router.push('/websites');
67+
} catch (error) {
68+
setError(
69+
`An error occurred while connecting your Vercel account: ${error instanceof Error ? error.message : 'Unknown error'}`
70+
);
71+
} finally {
72+
setIsProcessing(false);
73+
}
74+
};
75+
76+
const handleCancel = () => {
77+
router.push('/websites');
78+
};
79+
80+
if (error) {
81+
return (
82+
<div className="flex min-h-screen items-center justify-center bg-background p-4">
83+
<div className="w-full max-w-md rounded border border-destructive/20 bg-destructive/5 p-6 text-center">
84+
<div className="mb-4 text-4xl text-destructive">⚠️</div>
85+
<h1 className="mb-2 font-semibold text-destructive text-lg">
86+
Integration Failed
87+
</h1>
88+
<p className="mb-4 text-muted-foreground text-sm">{error}</p>
89+
<button
90+
className="rounded bg-primary px-4 py-2 font-medium text-primary-foreground hover:bg-primary/90"
91+
onClick={handleCancel}
92+
type="button"
93+
>
94+
Go Back to Dashboard
95+
</button>
96+
</div>
97+
</div>
98+
);
99+
}
100+
101+
if (isProcessing && !integrationData) {
102+
return (
103+
<div className="flex min-h-screen items-center justify-center bg-background">
104+
<div className="text-center">
105+
<div className="relative mb-4">
106+
<div className="absolute inset-0 animate-ping rounded-full bg-primary/20 blur-xl" />
107+
<SpinnerIcon className="relative mx-auto h-8 w-8 animate-spin text-primary" />
108+
</div>
109+
<h1 className="mb-2 font-semibold text-lg">
110+
Processing integration...
111+
</h1>
112+
<p className="text-muted-foreground text-sm">
113+
Please wait while we verify your request.
114+
</p>
115+
</div>
116+
</div>
117+
);
118+
}
119+
120+
return (
121+
<div className="flex min-h-screen items-center justify-center bg-background p-4">
122+
<div className="w-full max-w-lg rounded border bg-card p-6 shadow-lg">
123+
<div className="mb-6 text-center">
124+
<div className="mb-4 text-6xl">🔗</div>
125+
<h1 className="mb-2 font-bold text-2xl">Connect Vercel Account</h1>
126+
<p className="text-muted-foreground">
127+
You're about to connect your Vercel account to Databuddy
128+
</p>
129+
</div>
130+
131+
<div className="mb-6 space-y-3 rounded bg-muted/50 p-4">
132+
<h3 className="font-medium text-sm">Integration Details:</h3>
133+
{integrationData?.configurationId && (
134+
<div className="flex justify-between text-sm">
135+
<span className="text-muted-foreground">Configuration ID:</span>
136+
<span className="font-mono text-xs">
137+
{integrationData.configurationId}
138+
</span>
139+
</div>
140+
)}
141+
{integrationData?.teamId && (
142+
<div className="flex justify-between text-sm">
143+
<span className="text-muted-foreground">Team ID:</span>
144+
<span className="font-mono text-xs">
145+
{integrationData.teamId}
146+
</span>
147+
</div>
148+
)}
149+
<div className="flex justify-between text-sm">
150+
<span className="text-muted-foreground">Provider:</span>
151+
<span>Vercel</span>
152+
</div>
153+
</div>
154+
155+
<div className="mb-6 rounded border-blue-500 border-l-4 bg-blue-50 p-4 dark:bg-blue-950/20">
156+
<h4 className="mb-2 font-medium text-blue-800 text-sm dark:text-blue-200">
157+
What this integration will do:
158+
</h4>
159+
<ul className="space-y-1 text-blue-700 text-sm dark:text-blue-300">
160+
<li>• Access your Vercel projects and deployments</li>
161+
<li>• Monitor deployment analytics</li>
162+
<li>• Sync project data with Databuddy</li>
163+
<li>• Enable deployment notifications</li>
164+
</ul>
165+
</div>
166+
167+
<div className="flex gap-3">
168+
<button
169+
className="flex-1 rounded border border-input bg-background px-4 py-2 font-medium hover:bg-accent hover:text-accent-foreground"
170+
disabled={isProcessing}
171+
onClick={handleCancel}
172+
type="button"
173+
>
174+
Cancel
175+
</button>
176+
<button
177+
className="flex-1 rounded bg-primary px-4 py-2 font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
178+
disabled={isProcessing}
179+
onClick={handleConfirmIntegration}
180+
type="button"
181+
>
182+
{isProcessing ? (
183+
<>
184+
<SpinnerIcon className="mr-2 inline h-4 w-4 animate-spin" />
185+
Connecting...
186+
</>
187+
) : (
188+
'Connect Account'
189+
)}
190+
</button>
191+
</div>
192+
</div>
193+
</div>
194+
);
195+
}
196+
197+
export default function VercelCallbackPage() {
198+
return (
199+
<Suspense
200+
fallback={
201+
<div className="flex min-h-screen items-center justify-center bg-background">
202+
<SpinnerIcon className="h-8 w-8 animate-spin text-primary" />
203+
</div>
204+
}
205+
>
206+
<VercelCallbackContent />
207+
</Suspense>
208+
);
209+
}

0 commit comments

Comments
 (0)