Skip to content

Commit 8de1f91

Browse files
fait la mise a jour du webhook GitHub.
et aussi fait la mise a jour pou
1 parent ab6bceb commit 8de1f91

File tree

7 files changed

+420
-36
lines changed

7 files changed

+420
-36
lines changed

src/app/(app)/profile/page.tsx

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as z from 'zod';
1515
import { useEffect, useState, useActionState, startTransition } from 'react'; // Corrected import
1616
import { useToast } from '@/hooks/use-toast';
1717
import * as authService from '@/lib/authService';
18-
import { fetchUserGithubOAuthTokenAction, disconnectGithubAction, fetchGithubUserDetailsAction } from '@/app/(app)/projects/[id]/actions';
18+
import { fetchUserGithubOAuthTokenAction, disconnectGithubAction, fetchGithubUserDetailsAction, fetchDiscordUserDetailsAction, disconnectDiscordAction } from '@/app/(app)/projects/[id]/actions';
1919
import { useRouter } from 'next/navigation';
2020
import Link from 'next/link';
2121
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
@@ -36,6 +36,14 @@ interface GithubUserDetails {
3636
name: string | null;
3737
}
3838

39+
interface DiscordUserDetails {
40+
id: string;
41+
username: string;
42+
avatar: string | null;
43+
discriminator: string;
44+
}
45+
46+
3947
export default function ProfilePage() {
4048
const { user, isLoading: authLoading, refreshUser } = useAuth();
4149
const router = useRouter();
@@ -47,7 +55,11 @@ export default function ProfilePage() {
4755
const [githubUserDetails, setGithubUserDetails] = useState<GithubUserDetails | null>(null);
4856
const [isLoadingGithub, setIsLoadingGithub] = useState(true);
4957

50-
const [disconnectState, disconnectFormAction, isDisconnectPending] = useActionState(disconnectGithubAction, { success: false });
58+
const [discordUserDetails, setDiscordUserDetails] = useState<DiscordUserDetails | null>(null);
59+
const [isLoadingDiscord, setIsLoadingDiscord] = useState(true);
60+
61+
const [disconnectGithubState, disconnectGithubFormAction, isDisconnectGithubPending] = useActionState(disconnectGithubAction, { success: false });
62+
const [disconnectDiscordState, disconnectDiscordFormAction, isDisconnectDiscordPending] = useActionState(disconnectDiscordAction, { success: false });
5163

5264

5365
const form = useForm<ProfileFormValues>({
@@ -70,9 +82,11 @@ export default function ProfilePage() {
7082
}, [user, form]);
7183

7284
useEffect(() => {
73-
async function loadGithubData() {
85+
async function loadExternalData() {
7486
if (user) {
7587
setIsLoadingGithub(true);
88+
setIsLoadingDiscord(true);
89+
7690
const tokenData = await fetchUserGithubOAuthTokenAction();
7791
if (tokenData?.accessToken) {
7892
setGithubToken(tokenData.accessToken);
@@ -83,22 +97,37 @@ export default function ProfilePage() {
8397
setGithubUserDetails(null);
8498
}
8599
setIsLoadingGithub(false);
100+
101+
const discordDetails = await fetchDiscordUserDetailsAction();
102+
setDiscordUserDetails(discordDetails);
103+
setIsLoadingDiscord(false);
86104
}
87105
}
88-
if (!authLoading) loadGithubData();
106+
if (!authLoading) loadExternalData();
89107
}, [user, authLoading]);
90108

91109
useEffect(() => {
92-
if (!isDisconnectPending && disconnectState) {
93-
if (disconnectState.success && disconnectState.message) { // Check for message
94-
toast({ title: "Success", description: disconnectState.message });
110+
if (!isDisconnectGithubPending && disconnectGithubState) {
111+
if (disconnectGithubState.success && disconnectGithubState.message) {
112+
toast({ title: "Success", description: disconnectGithubState.message });
95113
setGithubToken(null);
96114
setGithubUserDetails(null);
97-
} else if (disconnectState.error) {
98-
toast({ variant: "destructive", title: "Error", description: disconnectState.error });
115+
} else if (disconnectGithubState.error) {
116+
toast({ variant: "destructive", title: "Error", description: disconnectGithubState.error });
99117
}
100118
}
101-
}, [disconnectState, isDisconnectPending, toast]);
119+
}, [disconnectGithubState, isDisconnectGithubPending, toast]);
120+
121+
useEffect(() => {
122+
if (!isDisconnectDiscordPending && disconnectDiscordState) {
123+
if (disconnectDiscordState.success && disconnectDiscordState.message) {
124+
toast({ title: "Success", description: disconnectDiscordState.message });
125+
setDiscordUserDetails(null);
126+
} else if (disconnectDiscordState.error) {
127+
toast({ variant: "destructive", title: "Error", description: disconnectDiscordState.error });
128+
}
129+
}
130+
}, [disconnectDiscordState, isDisconnectDiscordPending, toast]);
102131

103132

104133
if (authLoading) {
@@ -167,17 +196,27 @@ export default function ProfilePage() {
167196
};
168197

169198
const handleConnectGitHub = () => {
170-
// Redirect to the GitHub OAuth login flow
171199
window.location.href = `/api/auth/github/oauth/login?redirectTo=/profile`;
172200
};
173201

174202
const handleDisconnectGitHub = () => {
175-
const dummyFormData = new FormData(); // useActionState requires FormData
176-
startTransition(() => { // Corrected usage
177-
disconnectFormAction(dummyFormData);
203+
const dummyFormData = new FormData();
204+
startTransition(() => {
205+
disconnectGithubFormAction(dummyFormData);
178206
});
179207
};
180208

209+
const handleConnectDiscord = () => {
210+
window.location.href = `/api/auth/discord/oauth/login?redirectTo=/profile`;
211+
};
212+
213+
const handleDisconnectDiscord = () => {
214+
const dummyFormData = new FormData();
215+
startTransition(() => {
216+
disconnectDiscordFormAction(dummyFormData);
217+
});
218+
};
219+
181220

182221
return (
183222
<div className="space-y-8">
@@ -243,8 +282,9 @@ export default function ProfilePage() {
243282
{githubUserDetails && <Avatar className="h-5 w-5 inline-block"><AvatarImage src={githubUserDetails.avatar_url} /></Avatar>}
244283
</div>
245284
<div className="flex items-center space-x-2">
246-
<RadioGroupItem value="discord" id="r-discord" disabled />
247-
<Label htmlFor="r-discord" className="text-muted-foreground">Use Discord Profile Picture</Label>
285+
<RadioGroupItem value="discord" id="r-discord" disabled={!discordUserDetails} />
286+
<Label htmlFor="r-discord" className={!discordUserDetails ? "text-muted-foreground" : ""}>Use Discord Profile Picture</Label>
287+
{discordUserDetails?.avatar && <Avatar className="h-5 w-5 inline-block"><AvatarImage src={`https://cdn.discordapp.com/avatars/${discordUserDetails.id}/${discordUserDetails.avatar}.png`} /></Avatar>}
248288
</div>
249289
</RadioGroup>
250290
</Card>
@@ -324,8 +364,8 @@ export default function ProfilePage() {
324364
{isLoadingGithub ? (
325365
<Skeleton className="h-9 w-24" />
326366
) : githubToken ? (
327-
<Button variant="outline" onClick={handleDisconnectGitHub} disabled={isDisconnectPending}>
328-
{isDisconnectPending ? <Loader2 className="h-4 w-4 animate-spin mr-2"/> : <PowerOff className="mr-2 h-4 w-4" />}
367+
<Button variant="outline" onClick={handleDisconnectGitHub} disabled={isDisconnectGithubPending}>
368+
{isDisconnectGithubPending ? <Loader2 className="h-4 w-4 animate-spin mr-2"/> : <PowerOff className="mr-2 h-4 w-4" />}
329369
Disconnect
330370
</Button>
331371
) : (
@@ -336,19 +376,42 @@ export default function ProfilePage() {
336376
</div>
337377
</Card>
338378

339-
{/* Discord Connection (Placeholder) */}
379+
{/* Discord Connection */}
340380
<Card className="p-4 bg-muted/30">
341381
<div className="flex items-center justify-between">
342382
<div className="flex items-center gap-3">
343383
<MessageSquare className="h-8 w-8 text-indigo-500" />
344384
<div>
345385
<h4 className="font-semibold">Discord</h4>
346-
<p className="text-sm text-muted-foreground">Connect to receive notifications (Coming Soon).</p>
386+
{isLoadingDiscord ? (
387+
<Skeleton className="h-4 w-32 mt-1" />
388+
) : discordUserDetails ? (
389+
<div className="text-sm text-muted-foreground">
390+
Connected as: <span className="font-medium text-primary">{discordUserDetails.username}#{discordUserDetails.discriminator}</span>
391+
{discordUserDetails.avatar && (
392+
<Avatar className="h-5 w-5 inline-block ml-2 align-middle">
393+
<AvatarImage src={`https://cdn.discordapp.com/avatars/${discordUserDetails.id}/${discordUserDetails.avatar}.png`} alt={discordUserDetails.username} data-ai-hint="discord avatar" />
394+
<AvatarFallback>{getInitials(discordUserDetails.username)}</AvatarFallback>
395+
</Avatar>
396+
)}
397+
</div>
398+
) : (
399+
<p className="text-sm text-muted-foreground">Not Connected. Connect to enable DM notifications.</p>
400+
)}
347401
</div>
348402
</div>
349-
<Button disabled>
350-
<MessageSquare className="mr-2 h-4 w-4" /> Connect (Soon)
351-
</Button>
403+
{isLoadingDiscord ? (
404+
<Skeleton className="h-9 w-24" />
405+
) : discordUserDetails ? (
406+
<Button variant="outline" onClick={handleDisconnectDiscord} disabled={isDisconnectDiscordPending}>
407+
{isDisconnectDiscordPending ? <Loader2 className="h-4 w-4 animate-spin mr-2"/> : <PowerOff className="mr-2 h-4 w-4" />}
408+
Disconnect
409+
</Button>
410+
) : (
411+
<Button onClick={handleConnectDiscord}>
412+
<MessageSquare className="mr-2 h-4 w-4" /> Connect
413+
</Button>
414+
)}
352415
</div>
353416
</Card>
354417
</CardContent>

src/app/(app)/projects/[id]/page.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ import {
6262
type UpdateProjectDiscordSettingsFormState,
6363
deleteProjectAction,
6464
type DeleteProjectFormState,
65+
setupGithubWebhookAction,
66+
type SetupGithubWebhookFormState,
6567
} from './actions';
6668
import { Input } from '@/components/ui/input';
6769
import { Label } from '@/components/ui/label';
@@ -345,6 +347,7 @@ function ProjectDetailPageContent() {
345347
const [linkGithubState, linkProjectToGithubFormAction, isLinkGithubPending] = useActionState(linkProjectToGithubAction, { message: "", error: "" });
346348
const [updateDiscordSettingsState, updateDiscordSettingsFormAction, isUpdateDiscordSettingsPending] = useActionState(updateProjectDiscordSettingsAction, { message: "", error: "" });
347349
const [deleteProjectState, deleteProjectFormAction, isDeleteProjectPending] = useActionState(deleteProjectAction, {success: false});
350+
const [setupWebhookState, setupGithubWebhookFormAction, isSetupWebhookPending] = useActionState(setupGithubWebhookAction, { success: false });
348351

349352

350353
const loadTasks = useCallback(async () => {
@@ -763,6 +766,20 @@ function ProjectDetailPageContent() {
763766
}
764767
}, [deleteProjectState, isDeleteProjectPending, toast, router]);
765768

769+
useEffect(() => {
770+
if (!isSetupWebhookPending && setupWebhookState) {
771+
if (setupWebhookState.success) {
772+
toast({ title: "Success", description: setupWebhookState.message });
773+
if (setupWebhookState.project) {
774+
setProject(setupWebhookState.project);
775+
}
776+
}
777+
if (setupWebhookState.error) {
778+
toast({ variant: 'destructive', title: 'Webhook Error', description: setupWebhookState.error });
779+
}
780+
}
781+
}, [setupWebhookState, isSetupWebhookPending, toast]);
782+
766783

767784
useEffect(() => {
768785
if (project) {
@@ -1255,6 +1272,15 @@ function ProjectDetailPageContent() {
12551272
});
12561273
};
12571274

1275+
const handleSetupWebhook = () => {
1276+
if (!project) return;
1277+
const formData = new FormData();
1278+
formData.append('projectUuid', project.uuid);
1279+
startTransition(() => {
1280+
setupGithubWebhookFormAction(formData);
1281+
});
1282+
};
1283+
12581284
const copyToClipboard = (text: string, type: string) => {
12591285
navigator.clipboard.writeText(text);
12601286
toast({ title: `${type} Copied!`, description: `${type} URL copied to clipboard.` });
@@ -2203,14 +2229,15 @@ function ProjectDetailPageContent() {
22032229
<CardTitle className="flex items-center"><GitBranch className="mr-2 h-5 w-5"/>Activity Logs</CardTitle>
22042230
<CardDescription>View recent commits and activity from the linked GitHub repository.</CardDescription>
22052231
</div>
2206-
<Button variant="outline" size="sm" disabled>
2207-
<Github className="mr-2 h-4 w-4" /> Setup Webhook (Coming Soon)
2232+
<Button variant="outline" size="sm" onClick={handleSetupWebhook} disabled={!project.githubRepoUrl || !!project.githubWebhookId || isSetupWebhookPending || !canManageCodeSpace}>
2233+
{isSetupWebhookPending && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
2234+
{project.githubWebhookId ? <><CheckCircle className="h-4 w-4 mr-2 text-green-500" />Webhook Active</> : <><Github className="mr-2 h-4 w-4" />Setup Webhook</>}
22082235
</Button>
22092236
</CardHeader>
22102237
<CardContent className="text-center py-8 text-muted-foreground border-dashed border-2 rounded-md m-6">
22112238
<Terminal className="mx-auto h-12 w-12 opacity-50 mb-3" />
22122239
<p className="font-medium">GitHub Webhook Integration</p>
2213-
<p className="text-xs">This feature is planned for a future update. Once implemented, commit history will appear here.</p>
2240+
<p className="text-xs">Once the webhook is set up, commit history will appear here.</p>
22142241
</CardContent>
22152242
</Card>
22162243
</CardContent>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
2+
import { type NextRequest, NextResponse } from 'next/server';
3+
import { auth } from '@/lib/authEdge';
4+
import { storeUserDiscordToken } from '@/lib/db';
5+
6+
export async function GET(request: NextRequest) {
7+
const searchParams = request.nextUrl.searchParams;
8+
const code = searchParams.get('code');
9+
const stateFromDiscord = searchParams.get('state');
10+
11+
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
12+
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
13+
const NEXT_PUBLIC_APP_URL = process.env.NEXT_PUBLIC_APP_URL;
14+
15+
if (!DISCORD_CLIENT_ID || !DISCORD_CLIENT_SECRET || !NEXT_PUBLIC_APP_URL) {
16+
console.error('[Discord OAuth Callback] OAuth environment variables not configured.');
17+
return NextResponse.redirect(new URL('/profile?error=oauth_config_error', request.url));
18+
}
19+
20+
const storedStateCookie = request.cookies.get('discord_oauth_state');
21+
request.cookies.delete('discord_oauth_state');
22+
23+
if (!storedStateCookie) {
24+
console.error('[Discord OAuth Callback] Missing OAuth state cookie.');
25+
return NextResponse.redirect(new URL('/profile?error=oauth_state_missing', request.url));
26+
}
27+
28+
let storedStateData;
29+
try {
30+
storedStateData = JSON.parse(storedStateCookie.value);
31+
} catch (e) {
32+
console.error('[Discord OAuth Callback] Error parsing OAuth state cookie:', e);
33+
return NextResponse.redirect(new URL('/profile?error=oauth_state_invalid_parse', request.url));
34+
}
35+
36+
if (!stateFromDiscord || stateFromDiscord !== storedStateData.csrf) {
37+
console.error('[Discord OAuth Callback] OAuth state mismatch.');
38+
return NextResponse.redirect(new URL('/profile?error=oauth_state_mismatch', request.url));
39+
}
40+
41+
const session = await auth();
42+
if (!session?.user?.uuid) {
43+
console.error('[Discord OAuth Callback] No active FlowUp user session found.');
44+
return NextResponse.redirect(new URL('/login?error=oauth_no_session', request.url));
45+
}
46+
47+
if (!code) {
48+
const error = searchParams.get('error');
49+
const errorDescription = searchParams.get('error_description');
50+
return NextResponse.redirect(new URL(`/profile?error=oauth_missing_code&discord_error=${error || ''}&discord_desc=${errorDescription || ''}`, request.url));
51+
}
52+
53+
try {
54+
const tokenResponse = await fetch('https://discord.com/api/oauth2/token', {
55+
method: 'POST',
56+
headers: {
57+
'Content-Type': 'application/x-www-form-urlencoded',
58+
},
59+
body: new URLSearchParams({
60+
client_id: DISCORD_CLIENT_ID,
61+
client_secret: DISCORD_CLIENT_SECRET,
62+
grant_type: 'authorization_code',
63+
code,
64+
redirect_uri: `${NEXT_PUBLIC_APP_URL}/api/auth/discord/oauth/callback`,
65+
}),
66+
});
67+
68+
if (!tokenResponse.ok) {
69+
throw new Error(`Discord token exchange failed: ${await tokenResponse.text()}`);
70+
}
71+
const tokenData = await tokenResponse.json();
72+
73+
const userResponse = await fetch('https://discord.com/api/users/@me', {
74+
headers: {
75+
Authorization: `Bearer ${tokenData.access_token}`,
76+
},
77+
});
78+
79+
if (!userResponse.ok) {
80+
throw new Error(`Failed to fetch Discord user details: ${await userResponse.text()}`);
81+
}
82+
const userData = await userResponse.json();
83+
84+
await storeUserDiscordToken(session.user.uuid, {
85+
accessToken: tokenData.access_token,
86+
refreshToken: tokenData.refresh_token,
87+
expiresAt: Date.now() + tokenData.expires_in * 1000,
88+
scopes: tokenData.scope,
89+
discordUserId: userData.id,
90+
discordUsername: userData.username,
91+
discordAvatar: userData.avatar,
92+
});
93+
94+
const redirectPath = storedStateData.redirectTo || '/profile';
95+
const redirectUrlObj = new URL(redirectPath, request.nextUrl.origin);
96+
redirectUrlObj.searchParams.set('discord_oauth_status', 'success');
97+
98+
return NextResponse.redirect(redirectUrlObj);
99+
100+
} catch (error: any) {
101+
console.error('[Discord OAuth Callback] Error:', error);
102+
return NextResponse.redirect(new URL(`/profile?error=oauth_callback_error&message=${encodeURIComponent(error.message || 'Unknown error')}`, request.url));
103+
}
104+
}

0 commit comments

Comments
 (0)