diff --git a/.env.example b/.env.example
index ae3ccd6a..bce59406 100644
--- a/.env.example
+++ b/.env.example
@@ -61,3 +61,13 @@ NEXT_PUBLIC_LOG_AGGREGATION_URL=https://your-log-aggregation-endpoint.com/logs
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret
DISCORD_REDIRECT_URI=http://localhost:3000/api/auth/discord/callback
+
+# Google OAuth Configuration
+GOOGLE_CLIENT_ID=your_google_client_id
+GOOGLE_CLIENT_SECRET=your_google_client_secret
+GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/google/callback
+
+# GitHub OAuth Configuration
+GITHUB_CLIENT_ID=your_github_client_id
+GITHUB_CLIENT_SECRET=your_github_client_secret
+GITHUB_REDIRECT_URI=http://localhost:3000/api/auth/github/callback
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index 8788bb02..978dec61 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -13,6 +13,8 @@ import { SubmitButton } from '../../../components/forms/SubmitButton';
import { useMutation } from '../../../hooks/useMutation';
import { apiClient } from '@/lib/api';
import { DiscordButton } from '@/app/components/auth/DiscordButton';
+import { GoogleButton } from '@/app/components/auth/GoogleButton';
+import { GitHubButton } from '@/app/components/auth/GitHubButton';
export default function LoginPage() {
const [showPassword, setShowPassword] = useState(false);
@@ -23,6 +25,14 @@ export default function LoginPage() {
window.location.href = '/api/auth/discord';
};
+ const handleGoogleLogin = () => {
+ window.location.href = '/api/auth/google';
+ };
+
+ const handleGitHubLogin = () => {
+ window.location.href = '/api/auth/github';
+ };
+
const {
register,
handleSubmit,
@@ -188,39 +198,8 @@ export default function LoginPage() {
-
-
+
+
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx
index c58a9cb7..f10fe9ea 100644
--- a/src/app/(auth)/signup/page.tsx
+++ b/src/app/(auth)/signup/page.tsx
@@ -13,6 +13,8 @@ import { SubmitButton } from '../../../components/forms/SubmitButton';
import { useMutation } from '../../../hooks/useMutation';
import { apiClient } from '@/lib/api';
import { DiscordButton } from '@/app/components/auth/DiscordButton';
+import { GoogleButton } from '@/app/components/auth/GoogleButton';
+import { GitHubButton } from '@/app/components/auth/GitHubButton';
export default function SignupPage() {
const [showPassword, setShowPassword] = useState(false);
@@ -24,6 +26,14 @@ export default function SignupPage() {
window.location.href = '/api/auth/discord';
};
+ const handleGoogleSignup = () => {
+ window.location.href = '/api/auth/google';
+ };
+
+ const handleGitHubSignup = () => {
+ window.location.href = '/api/auth/github';
+ };
+
const {
register,
handleSubmit,
@@ -260,40 +270,9 @@ export default function SignupPage() {
{/* Social buttons */}
-
+
-
+
diff --git a/src/app/api/auth/github/callback/route.ts b/src/app/api/auth/github/callback/route.ts
new file mode 100644
index 00000000..77fcf01e
--- /dev/null
+++ b/src/app/api/auth/github/callback/route.ts
@@ -0,0 +1,98 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { withRateLimit } from '@/lib/ratelimit';
+import { exchangeCodeForToken, getGitHubUser, getGitHubAvatarUrl } from '@/lib/github/oauth';
+import type { AuthResponseDTO, AuthErrorDTO } from '@/types/api/auth.dto';
+import { edgeLog } from '@/../infra/edge-config';
+
+export const runtime = 'edge';
+
+/**
+ * GET /api/auth/github/callback
+ * Handles GitHub OAuth2 callback and creates/updates user session
+ */
+export async function GET(
+ request: NextRequest,
+): Promise> {
+ edgeLog('info', '/api/auth/github/callback', 'GET request received');
+
+ const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
+ if (rateLimitResponse) return rateLimitResponse as NextResponse;
+
+ try {
+ const searchParams = request.nextUrl.searchParams;
+ const code = searchParams.get('code');
+ const state = searchParams.get('state');
+ const error = searchParams.get('error');
+
+ // Check for OAuth errors
+ if (error) {
+ edgeLog('error', '/api/auth/github/callback', `OAuth error: ${error}`);
+ return addHeaders(
+ NextResponse.json({ message: `GitHub OAuth error: ${error}` }, { status: 400 }),
+ ) as NextResponse;
+ }
+
+ if (!code) {
+ return addHeaders(
+ NextResponse.json({ message: 'Authorization code is required' }, { status: 400 }),
+ ) as NextResponse;
+ }
+
+ // Verify state parameter to prevent CSRF attacks
+ const storedState = request.cookies.get('github_oauth_state')?.value;
+ if (!state || state !== storedState) {
+ edgeLog('error', '/api/auth/github/callback', 'Invalid state parameter');
+ return addHeaders(
+ NextResponse.json({ message: 'Invalid state parameter' }, { status: 400 }),
+ ) as NextResponse;
+ }
+
+ // Exchange code for access token
+ const tokenResponse = await exchangeCodeForToken(code);
+
+ // Get GitHub user information
+ const githubUser = await getGitHubUser(tokenResponse.access_token);
+
+ // Validate that user has email
+ if (!githubUser.email) {
+ return addHeaders(
+ NextResponse.json(
+ { message: 'GitHub account must have a verified email' },
+ { status: 400 },
+ ),
+ ) as NextResponse;
+ }
+
+ const mockUserId = Math.random().toString(36).substring(2, 9);
+ const mockToken = `mock-jwt-token-${Date.now()}`;
+
+ // Clear the state cookie
+ const response = NextResponse.json(
+ {
+ message: 'GitHub authentication successful',
+ user: {
+ id: mockUserId,
+ name: githubUser.name || githubUser.login,
+ email: githubUser.email,
+ avatar: getGitHubAvatarUrl(githubUser),
+ provider: 'github',
+ providerId: String(githubUser.id),
+ },
+ token: mockToken,
+ },
+ { status: 200 },
+ );
+
+ // Clear the state cookie
+ response.cookies.delete('github_oauth_state');
+
+ return addHeaders(response) as NextResponse;
+ } catch (error) {
+ edgeLog('error', '/api/auth/github/callback', `Error: ${error}`);
+ console.error('GitHub OAuth callback error:', error);
+
+ return addHeaders(
+ NextResponse.json({ message: 'Internal server error' }, { status: 500 }),
+ ) as NextResponse;
+ }
+}
diff --git a/src/app/api/auth/github/route.ts b/src/app/api/auth/github/route.ts
new file mode 100644
index 00000000..53973bee
--- /dev/null
+++ b/src/app/api/auth/github/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { withRateLimit } from '@/lib/ratelimit';
+import { generateState, getGitHubAuthUrl } from '@/lib/github/oauth';
+import { edgeLog } from '@/../infra/edge-config';
+
+export const runtime = 'edge';
+
+/**
+ * GET /api/auth/github
+ * Initiates GitHub OAuth2 flow by redirecting to GitHub's authorization page
+ */
+export async function GET(request: NextRequest): Promise {
+ edgeLog('info', '/api/auth/github', 'GET request received');
+
+ const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
+ if (rateLimitResponse) return rateLimitResponse as NextResponse;
+
+ try {
+ const state = generateState();
+ const authUrl = getGitHubAuthUrl(state);
+
+ // Store state in a cookie for verification during callback
+ const response = NextResponse.redirect(authUrl);
+ response.cookies.set('github_oauth_state', state, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ maxAge: 60 * 10, // 10 minutes
+ });
+
+ return addHeaders(response) as NextResponse;
+ } catch (error) {
+ edgeLog('error', '/api/auth/github', `Error: ${error}`);
+
+ return addHeaders(
+ NextResponse.json({ message: 'Failed to initiate GitHub OAuth' }, { status: 500 }),
+ ) as NextResponse;
+ }
+}
diff --git a/src/app/api/auth/google/callback/route.ts b/src/app/api/auth/google/callback/route.ts
new file mode 100644
index 00000000..dd3f4c40
--- /dev/null
+++ b/src/app/api/auth/google/callback/route.ts
@@ -0,0 +1,105 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { withRateLimit } from '@/lib/ratelimit';
+import { exchangeCodeForToken, getGoogleUser, getGoogleAvatarUrl } from '@/lib/google/oauth';
+import type { AuthResponseDTO, AuthErrorDTO } from '@/types/api/auth.dto';
+import { edgeLog } from '@/../infra/edge-config';
+
+export const runtime = 'edge';
+
+/**
+ * GET /api/auth/google/callback
+ * Handles Google OAuth2 callback and creates/updates user session
+ */
+export async function GET(
+ request: NextRequest,
+): Promise> {
+ edgeLog('info', '/api/auth/google/callback', 'GET request received');
+
+ const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
+ if (rateLimitResponse) return rateLimitResponse as NextResponse;
+
+ try {
+ const searchParams = request.nextUrl.searchParams;
+ const code = searchParams.get('code');
+ const state = searchParams.get('state');
+ const error = searchParams.get('error');
+
+ // Check for OAuth errors
+ if (error) {
+ edgeLog('error', '/api/auth/google/callback', `OAuth error: ${error}`);
+ return addHeaders(
+ NextResponse.json({ message: `Google OAuth error: ${error}` }, { status: 400 }),
+ ) as NextResponse;
+ }
+
+ if (!code) {
+ return addHeaders(
+ NextResponse.json({ message: 'Authorization code is required' }, { status: 400 }),
+ ) as NextResponse;
+ }
+
+ // Verify state parameter to prevent CSRF attacks
+ const storedState = request.cookies.get('google_oauth_state')?.value;
+ if (!state || state !== storedState) {
+ edgeLog('error', '/api/auth/google/callback', 'Invalid state parameter');
+ return addHeaders(
+ NextResponse.json({ message: 'Invalid state parameter' }, { status: 400 }),
+ ) as NextResponse;
+ }
+
+ // Exchange code for access token
+ const tokenResponse = await exchangeCodeForToken(code);
+
+ // Get Google user information
+ const googleUser = await getGoogleUser(tokenResponse.access_token);
+
+ // Validate that user has email
+ if (!googleUser.email) {
+ return addHeaders(
+ NextResponse.json(
+ { message: 'Google account must have an email' },
+ { status: 400 },
+ ),
+ ) as NextResponse;
+ }
+
+ // Validate email is verified
+ if (!googleUser.verified_email) {
+ return addHeaders(
+ NextResponse.json({ message: 'Google email must be verified' }, { status: 400 }),
+ ) as NextResponse;
+ }
+
+ const mockUserId = Math.random().toString(36).substring(2, 9);
+ const mockToken = `mock-jwt-token-${Date.now()}`;
+
+ // Clear the state cookie
+ const response = NextResponse.json(
+ {
+ message: 'Google authentication successful',
+ user: {
+ id: mockUserId,
+ name: googleUser.name,
+ email: googleUser.email,
+ avatar: getGoogleAvatarUrl(googleUser),
+ provider: 'google',
+ providerId: googleUser.id,
+ },
+ token: mockToken,
+ },
+ { status: 200 },
+ );
+
+ // Clear the state cookie
+ response.cookies.delete('google_oauth_state');
+
+ return addHeaders(response) as NextResponse;
+ } catch (error) {
+ edgeLog('error', '/api/auth/google/callback', `Error: ${error}`);
+ console.error('Google OAuth callback error:', error);
+
+ return addHeaders(
+ NextResponse.json({ message: 'Internal server error' }, { status: 500 }),
+ ) as NextResponse;
+ }
+}
diff --git a/src/app/api/auth/google/route.ts b/src/app/api/auth/google/route.ts
new file mode 100644
index 00000000..0e56fc12
--- /dev/null
+++ b/src/app/api/auth/google/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { withRateLimit } from '@/lib/ratelimit';
+import { generateState, getGoogleAuthUrl } from '@/lib/google/oauth';
+import { edgeLog } from '@/../infra/edge-config';
+
+export const runtime = 'edge';
+
+/**
+ * GET /api/auth/google
+ * Initiates Google OAuth2 flow by redirecting to Google's authorization page
+ */
+export async function GET(request: NextRequest): Promise {
+ edgeLog('info', '/api/auth/google', 'GET request received');
+
+ const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
+ if (rateLimitResponse) return rateLimitResponse as NextResponse;
+
+ try {
+ const state = generateState();
+ const authUrl = getGoogleAuthUrl(state);
+
+ // Store state in a cookie for verification during callback
+ const response = NextResponse.redirect(authUrl);
+ response.cookies.set('google_oauth_state', state, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ maxAge: 60 * 10, // 10 minutes
+ });
+
+ return addHeaders(response) as NextResponse;
+ } catch (error) {
+ edgeLog('error', '/api/auth/google', `Error: ${error}`);
+
+ return addHeaders(
+ NextResponse.json({ message: 'Failed to initiate Google OAuth' }, { status: 500 }),
+ ) as NextResponse;
+ }
+}
diff --git a/src/app/components/auth/GitHubButton.tsx b/src/app/components/auth/GitHubButton.tsx
new file mode 100644
index 00000000..9da13f89
--- /dev/null
+++ b/src/app/components/auth/GitHubButton.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+interface GitHubButtonProps {
+ onClick: () => void;
+ disabled?: boolean;
+}
+
+export const GitHubButton = ({ onClick, disabled = false }: GitHubButtonProps) => {
+ return (
+
+
+ GitHub
+
+ );
+};
diff --git a/src/app/components/auth/GoogleButton.tsx b/src/app/components/auth/GoogleButton.tsx
new file mode 100644
index 00000000..05b74b1e
--- /dev/null
+++ b/src/app/components/auth/GoogleButton.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+interface GoogleButtonProps {
+ onClick: () => void;
+ disabled?: boolean;
+}
+
+export const GoogleButton = ({ onClick, disabled = false }: GoogleButtonProps) => {
+ return (
+
+
+ Google
+
+ );
+};
diff --git a/src/lib/github/oauth.ts b/src/lib/github/oauth.ts
new file mode 100644
index 00000000..05de6569
--- /dev/null
+++ b/src/lib/github/oauth.ts
@@ -0,0 +1,136 @@
+/**
+ * GitHub OAuth2 Integration
+ * Handles GitHub OAuth2 flow for authentication
+ */
+
+export interface GitHubUser {
+ id: number;
+ login: string;
+ name: string | null;
+ email: string | null;
+ avatar_url: string;
+ html_url: string;
+ bio: string | null;
+ location: string | null;
+ blog: string | null;
+ twitter_username: string | null;
+ company: string | null;
+}
+
+export interface GitHubTokenResponse {
+ access_token: string;
+ token_type: string;
+ scope: string;
+}
+
+const GITHUB_API_BASE = 'https://api.github.com';
+const GITHUB_OAUTH_BASE = 'https://github.com/login/oauth';
+
+/**
+ * Get GitHub OAuth authorization URL
+ */
+export function getGitHubAuthUrl(state: string): string {
+ const clientId = process.env.GITHUB_CLIENT_ID;
+ const redirectUri = process.env.GITHUB_REDIRECT_URI;
+ const scope = 'read:user user:email';
+
+ if (!clientId || !redirectUri) {
+ throw new Error('GitHub OAuth configuration is missing');
+ }
+
+ const params = new URLSearchParams({
+ client_id: clientId,
+ redirect_uri: redirectUri,
+ scope,
+ state,
+ });
+
+ return `${GITHUB_OAUTH_BASE}/authorize?${params.toString()}`;
+}
+
+/**
+ * Exchange authorization code for access token
+ */
+export async function exchangeCodeForToken(code: string): Promise {
+ const clientId = process.env.GITHUB_CLIENT_ID;
+ const clientSecret = process.env.GITHUB_CLIENT_SECRET;
+ const redirectUri = process.env.GITHUB_REDIRECT_URI;
+
+ if (!clientId || !clientSecret || !redirectUri) {
+ throw new Error('GitHub OAuth configuration is missing');
+ }
+
+ const response = await fetch(`${GITHUB_OAUTH_BASE}/access_token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Accept: 'application/json',
+ },
+ body: new URLSearchParams({
+ client_id: clientId,
+ client_secret: clientSecret,
+ code,
+ redirect_uri: redirectUri,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Failed to exchange code for token: ${error}`);
+ }
+
+ return response.json();
+}
+
+/**
+ * Get GitHub user information using access token
+ */
+export async function getGitHubUser(accessToken: string): Promise {
+ const response = await fetch(`${GITHUB_API_BASE}/user`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ Accept: 'application/vnd.github.v3+json',
+ },
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Failed to fetch GitHub user: ${error}`);
+ }
+
+ const user = await response.json();
+
+ // If user doesn't have public email, fetch primary email
+ if (!user.email) {
+ const emailResponse = await fetch(`${GITHUB_API_BASE}/user/emails`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ Accept: 'application/vnd.github.v3+json',
+ },
+ });
+
+ if (emailResponse.ok) {
+ const emails = await emailResponse.json();
+ const primaryEmail = emails.find((e: any) => e.primary && e.verified);
+ if (primaryEmail) {
+ user.email = primaryEmail.email;
+ }
+ }
+ }
+
+ return user;
+}
+
+/**
+ * Generate a random state parameter for OAuth
+ */
+export function generateState(): string {
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
+}
+
+/**
+ * Get GitHub avatar URL
+ */
+export function getGitHubAvatarUrl(user: GitHubUser): string {
+ return user.avatar_url || '';
+}
diff --git a/src/lib/google/oauth.ts b/src/lib/google/oauth.ts
new file mode 100644
index 00000000..af2a3036
--- /dev/null
+++ b/src/lib/google/oauth.ts
@@ -0,0 +1,118 @@
+/**
+ * Google OAuth2 Integration
+ * Handles Google OAuth2 flow for authentication
+ */
+
+export interface GoogleUser {
+ id: string;
+ email: string;
+ verified_email: boolean;
+ name: string;
+ given_name: string;
+ family_name: string;
+ picture: string;
+ locale: string;
+}
+
+export interface GoogleTokenResponse {
+ access_token: string;
+ token_type: string;
+ expires_in: number;
+ refresh_token?: string;
+ scope: string;
+ id_token: string;
+}
+
+const GOOGLE_API_BASE = 'https://www.googleapis.com';
+const GOOGLE_OAUTH_BASE = 'https://accounts.google.com/o/oauth2/v2';
+
+/**
+ * Get Google OAuth authorization URL
+ */
+export function getGoogleAuthUrl(state: string): string {
+ const clientId = process.env.GOOGLE_CLIENT_ID;
+ const redirectUri = process.env.GOOGLE_REDIRECT_URI;
+ const scope = 'openid email profile';
+
+ if (!clientId || !redirectUri) {
+ throw new Error('Google OAuth configuration is missing');
+ }
+
+ const params = new URLSearchParams({
+ client_id: clientId,
+ redirect_uri: redirectUri,
+ response_type: 'code',
+ scope,
+ state,
+ access_type: 'offline',
+ prompt: 'consent',
+ });
+
+ return `${GOOGLE_OAUTH_BASE}/auth?${params.toString()}`;
+}
+
+/**
+ * Exchange authorization code for access token
+ */
+export async function exchangeCodeForToken(code: string): Promise {
+ const clientId = process.env.GOOGLE_CLIENT_ID;
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
+ const redirectUri = process.env.GOOGLE_REDIRECT_URI;
+
+ if (!clientId || !clientSecret || !redirectUri) {
+ throw new Error('Google OAuth configuration is missing');
+ }
+
+ const response = await fetch(`${GOOGLE_API_BASE}/oauth2/v4/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams({
+ client_id: clientId,
+ client_secret: clientSecret,
+ grant_type: 'authorization_code',
+ code,
+ redirect_uri: redirectUri,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Failed to exchange code for token: ${error}`);
+ }
+
+ return response.json();
+}
+
+/**
+ * Get Google user information using access token
+ */
+export async function getGoogleUser(accessToken: string): Promise {
+ const response = await fetch(`${GOOGLE_API_BASE}/oauth2/v2/userinfo`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Failed to fetch Google user: ${error}`);
+ }
+
+ return response.json();
+}
+
+/**
+ * Generate a random state parameter for OAuth
+ */
+export function generateState(): string {
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
+}
+
+/**
+ * Get Google avatar URL
+ */
+export function getGoogleAvatarUrl(user: GoogleUser): string {
+ return user.picture || '';
+}