Skip to content

Commit 8a33d28

Browse files
committed
Google and GitHub OAuth buttons are non-functional — missing click handlers
1 parent 2de8fc0 commit 8a33d28

11 files changed

Lines changed: 646 additions & 66 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,13 @@ NEXT_PUBLIC_LOG_AGGREGATION_URL=https://your-log-aggregation-endpoint.com/logs
6161
DISCORD_CLIENT_ID=your_discord_client_id
6262
DISCORD_CLIENT_SECRET=your_discord_client_secret
6363
DISCORD_REDIRECT_URI=http://localhost:3000/api/auth/discord/callback
64+
65+
# Google OAuth Configuration
66+
GOOGLE_CLIENT_ID=your_google_client_id
67+
GOOGLE_CLIENT_SECRET=your_google_client_secret
68+
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/google/callback
69+
70+
# GitHub OAuth Configuration
71+
GITHUB_CLIENT_ID=your_github_client_id
72+
GITHUB_CLIENT_SECRET=your_github_client_secret
73+
GITHUB_REDIRECT_URI=http://localhost:3000/api/auth/github/callback

src/app/(auth)/login/page.tsx

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { SubmitButton } from '../../../components/forms/SubmitButton';
1313
import { useMutation } from '../../../hooks/useMutation';
1414
import { apiClient } from '@/lib/api';
1515
import { DiscordButton } from '@/app/components/auth/DiscordButton';
16+
import { GoogleButton } from '@/app/components/auth/GoogleButton';
17+
import { GitHubButton } from '@/app/components/auth/GitHubButton';
1618

1719
export default function LoginPage() {
1820
const [showPassword, setShowPassword] = useState(false);
@@ -23,6 +25,14 @@ export default function LoginPage() {
2325
window.location.href = '/api/auth/discord';
2426
};
2527

28+
const handleGoogleLogin = () => {
29+
window.location.href = '/api/auth/google';
30+
};
31+
32+
const handleGitHubLogin = () => {
33+
window.location.href = '/api/auth/github';
34+
};
35+
2636
const {
2737
register,
2838
handleSubmit,
@@ -188,39 +198,8 @@ export default function LoginPage() {
188198

189199
<div className="grid grid-cols-3 gap-4">
190200
<DiscordButton onClick={handleDiscordLogin} />
191-
<button
192-
type="button"
193-
className="px-4 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors flex items-center justify-center gap-2 text-sm font-medium text-gray-700"
194-
>
195-
<svg className="w-5 h-5" viewBox="0 0 24 24">
196-
<path
197-
fill="#4285F4"
198-
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
199-
/>
200-
<path
201-
fill="#34A853"
202-
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
203-
/>
204-
<path
205-
fill="#FBBC05"
206-
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
207-
/>
208-
<path
209-
fill="#EA4335"
210-
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
211-
/>
212-
</svg>
213-
<span>Google</span>
214-
</button>
215-
<button
216-
type="button"
217-
className="px-4 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors flex items-center justify-center gap-2 text-sm font-medium text-gray-700"
218-
>
219-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
220-
<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" />
221-
</svg>
222-
<span>GitHub</span>
223-
</button>
201+
<GoogleButton onClick={handleGoogleLogin} />
202+
<GitHubButton onClick={handleGitHubLogin} />
224203
</div>
225204
</div>
226205
</div>

src/app/(auth)/signup/page.tsx

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { SubmitButton } from '../../../components/forms/SubmitButton';
1313
import { useMutation } from '../../../hooks/useMutation';
1414
import { apiClient } from '@/lib/api';
1515
import { DiscordButton } from '@/app/components/auth/DiscordButton';
16+
import { GoogleButton } from '@/app/components/auth/GoogleButton';
17+
import { GitHubButton } from '@/app/components/auth/GitHubButton';
1618

1719
export default function SignupPage() {
1820
const [showPassword, setShowPassword] = useState(false);
@@ -24,6 +26,14 @@ export default function SignupPage() {
2426
window.location.href = '/api/auth/discord';
2527
};
2628

29+
const handleGoogleSignup = () => {
30+
window.location.href = '/api/auth/google';
31+
};
32+
33+
const handleGitHubSignup = () => {
34+
window.location.href = '/api/auth/github';
35+
};
36+
2737
const {
2838
register,
2939
handleSubmit,
@@ -260,40 +270,9 @@ export default function SignupPage() {
260270
{/* Social buttons */}
261271
<div className="grid grid-cols-3 gap-4">
262272
<DiscordButton onClick={handleDiscordSignup} />
263-
<button
264-
type="button"
265-
className="px-4 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors flex items-center justify-center gap-2"
266-
>
267-
<svg className="w-5 h-5" viewBox="0 0 24 24">
268-
<path
269-
fill="#4285F4"
270-
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
271-
/>
272-
<path
273-
fill="#34A853"
274-
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
275-
/>
276-
<path
277-
fill="#FBBC05"
278-
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
279-
/>
280-
<path
281-
fill="#EA4335"
282-
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
283-
/>
284-
</svg>
285-
<span className="text-sm font-medium text-gray-700">Google</span>
286-
</button>
273+
<GoogleButton onClick={handleGoogleSignup} />
287274

288-
<button
289-
type="button"
290-
className="px-4 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors flex items-center justify-center gap-2"
291-
>
292-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
293-
<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" />
294-
</svg>
295-
<span className="text-sm font-medium text-gray-700">GitHub</span>
296-
</button>
275+
<GitHubButton onClick={handleGitHubSignup} />
297276
</div>
298277
</div>
299278
</motion.div>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { withRateLimit } from '@/lib/ratelimit';
3+
import { exchangeCodeForToken, getGitHubUser, getGitHubAvatarUrl } from '@/lib/github/oauth';
4+
import type { AuthResponseDTO, AuthErrorDTO } from '@/types/api/auth.dto';
5+
import { edgeLog } from '@/../infra/edge-config';
6+
7+
export const runtime = 'edge';
8+
9+
/**
10+
* GET /api/auth/github/callback
11+
* Handles GitHub OAuth2 callback and creates/updates user session
12+
*/
13+
export async function GET(
14+
request: NextRequest,
15+
): Promise<NextResponse<AuthResponseDTO | AuthErrorDTO>> {
16+
edgeLog('info', '/api/auth/github/callback', 'GET request received');
17+
18+
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
19+
if (rateLimitResponse) return rateLimitResponse as NextResponse;
20+
21+
try {
22+
const searchParams = request.nextUrl.searchParams;
23+
const code = searchParams.get('code');
24+
const state = searchParams.get('state');
25+
const error = searchParams.get('error');
26+
27+
// Check for OAuth errors
28+
if (error) {
29+
edgeLog('error', '/api/auth/github/callback', `OAuth error: ${error}`);
30+
return addHeaders(
31+
NextResponse.json({ message: `GitHub OAuth error: ${error}` }, { status: 400 }),
32+
) as NextResponse;
33+
}
34+
35+
if (!code) {
36+
return addHeaders(
37+
NextResponse.json({ message: 'Authorization code is required' }, { status: 400 }),
38+
) as NextResponse;
39+
}
40+
41+
// Verify state parameter to prevent CSRF attacks
42+
const storedState = request.cookies.get('github_oauth_state')?.value;
43+
if (!state || state !== storedState) {
44+
edgeLog('error', '/api/auth/github/callback', 'Invalid state parameter');
45+
return addHeaders(
46+
NextResponse.json({ message: 'Invalid state parameter' }, { status: 400 }),
47+
) as NextResponse;
48+
}
49+
50+
// Exchange code for access token
51+
const tokenResponse = await exchangeCodeForToken(code);
52+
53+
// Get GitHub user information
54+
const githubUser = await getGitHubUser(tokenResponse.access_token);
55+
56+
// Validate that user has email
57+
if (!githubUser.email) {
58+
return addHeaders(
59+
NextResponse.json(
60+
{ message: 'GitHub account must have a verified email' },
61+
{ status: 400 },
62+
),
63+
) as NextResponse;
64+
}
65+
66+
const mockUserId = Math.random().toString(36).substring(2, 9);
67+
const mockToken = `mock-jwt-token-${Date.now()}`;
68+
69+
// Clear the state cookie
70+
const response = NextResponse.json(
71+
{
72+
message: 'GitHub authentication successful',
73+
user: {
74+
id: mockUserId,
75+
name: githubUser.name || githubUser.login,
76+
email: githubUser.email,
77+
avatar: getGitHubAvatarUrl(githubUser),
78+
provider: 'github',
79+
providerId: String(githubUser.id),
80+
},
81+
token: mockToken,
82+
},
83+
{ status: 200 },
84+
);
85+
86+
// Clear the state cookie
87+
response.cookies.delete('github_oauth_state');
88+
89+
return addHeaders(response) as NextResponse;
90+
} catch (error) {
91+
edgeLog('error', '/api/auth/github/callback', `Error: ${error}`);
92+
console.error('GitHub OAuth callback error:', error);
93+
94+
return addHeaders(
95+
NextResponse.json({ message: 'Internal server error' }, { status: 500 }),
96+
) as NextResponse;
97+
}
98+
}

src/app/api/auth/github/route.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { withRateLimit } from '@/lib/ratelimit';
3+
import { generateState, getGitHubAuthUrl } from '@/lib/github/oauth';
4+
import { edgeLog } from '@/../infra/edge-config';
5+
6+
export const runtime = 'edge';
7+
8+
/**
9+
* GET /api/auth/github
10+
* Initiates GitHub OAuth2 flow by redirecting to GitHub's authorization page
11+
*/
12+
export async function GET(request: NextRequest): Promise<NextResponse> {
13+
edgeLog('info', '/api/auth/github', 'GET request received');
14+
15+
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
16+
if (rateLimitResponse) return rateLimitResponse as NextResponse;
17+
18+
try {
19+
const state = generateState();
20+
const authUrl = getGitHubAuthUrl(state);
21+
22+
// Store state in a cookie for verification during callback
23+
const response = NextResponse.redirect(authUrl);
24+
response.cookies.set('github_oauth_state', state, {
25+
httpOnly: true,
26+
secure: process.env.NODE_ENV === 'production',
27+
sameSite: 'lax',
28+
maxAge: 60 * 10, // 10 minutes
29+
});
30+
31+
return addHeaders(response) as NextResponse;
32+
} catch (error) {
33+
edgeLog('error', '/api/auth/github', `Error: ${error}`);
34+
35+
return addHeaders(
36+
NextResponse.json({ message: 'Failed to initiate GitHub OAuth' }, { status: 500 }),
37+
) as NextResponse;
38+
}
39+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { withRateLimit } from '@/lib/ratelimit';
3+
import { exchangeCodeForToken, getGoogleUser, getGoogleAvatarUrl } from '@/lib/google/oauth';
4+
import type { AuthResponseDTO, AuthErrorDTO } from '@/types/api/auth.dto';
5+
import { edgeLog } from '@/../infra/edge-config';
6+
7+
export const runtime = 'edge';
8+
9+
/**
10+
* GET /api/auth/google/callback
11+
* Handles Google OAuth2 callback and creates/updates user session
12+
*/
13+
export async function GET(
14+
request: NextRequest,
15+
): Promise<NextResponse<AuthResponseDTO | AuthErrorDTO>> {
16+
edgeLog('info', '/api/auth/google/callback', 'GET request received');
17+
18+
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
19+
if (rateLimitResponse) return rateLimitResponse as NextResponse;
20+
21+
try {
22+
const searchParams = request.nextUrl.searchParams;
23+
const code = searchParams.get('code');
24+
const state = searchParams.get('state');
25+
const error = searchParams.get('error');
26+
27+
// Check for OAuth errors
28+
if (error) {
29+
edgeLog('error', '/api/auth/google/callback', `OAuth error: ${error}`);
30+
return addHeaders(
31+
NextResponse.json({ message: `Google OAuth error: ${error}` }, { status: 400 }),
32+
) as NextResponse;
33+
}
34+
35+
if (!code) {
36+
return addHeaders(
37+
NextResponse.json({ message: 'Authorization code is required' }, { status: 400 }),
38+
) as NextResponse;
39+
}
40+
41+
// Verify state parameter to prevent CSRF attacks
42+
const storedState = request.cookies.get('google_oauth_state')?.value;
43+
if (!state || state !== storedState) {
44+
edgeLog('error', '/api/auth/google/callback', 'Invalid state parameter');
45+
return addHeaders(
46+
NextResponse.json({ message: 'Invalid state parameter' }, { status: 400 }),
47+
) as NextResponse;
48+
}
49+
50+
// Exchange code for access token
51+
const tokenResponse = await exchangeCodeForToken(code);
52+
53+
// Get Google user information
54+
const googleUser = await getGoogleUser(tokenResponse.access_token);
55+
56+
// Validate that user has email
57+
if (!googleUser.email) {
58+
return addHeaders(
59+
NextResponse.json(
60+
{ message: 'Google account must have an email' },
61+
{ status: 400 },
62+
),
63+
) as NextResponse;
64+
}
65+
66+
// Validate email is verified
67+
if (!googleUser.verified_email) {
68+
return addHeaders(
69+
NextResponse.json({ message: 'Google email must be verified' }, { status: 400 }),
70+
) as NextResponse;
71+
}
72+
73+
const mockUserId = Math.random().toString(36).substring(2, 9);
74+
const mockToken = `mock-jwt-token-${Date.now()}`;
75+
76+
// Clear the state cookie
77+
const response = NextResponse.json(
78+
{
79+
message: 'Google authentication successful',
80+
user: {
81+
id: mockUserId,
82+
name: googleUser.name,
83+
email: googleUser.email,
84+
avatar: getGoogleAvatarUrl(googleUser),
85+
provider: 'google',
86+
providerId: googleUser.id,
87+
},
88+
token: mockToken,
89+
},
90+
{ status: 200 },
91+
);
92+
93+
// Clear the state cookie
94+
response.cookies.delete('google_oauth_state');
95+
96+
return addHeaders(response) as NextResponse;
97+
} catch (error) {
98+
edgeLog('error', '/api/auth/google/callback', `Error: ${error}`);
99+
console.error('Google OAuth callback error:', error);
100+
101+
return addHeaders(
102+
NextResponse.json({ message: 'Internal server error' }, { status: 500 }),
103+
) as NextResponse;
104+
}
105+
}

0 commit comments

Comments
 (0)