diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a5b2d1e --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=http://localhost:4000 +VITE_SUPABASE_URL="" +VITE_SUPABASE_ANON_KEY="" diff --git a/package.json b/package.json index 2392ad5..1d3139f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@emotion/styled": "^11.13.5", "@mui/icons-material": "^6.1.1", "@mui/material": "^6.1.1", + "@supabase/supabase-js": "^2.81.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", diff --git a/server/index.js b/server/index.js index 562ccb7..d88340b 100644 --- a/server/index.js +++ b/server/index.js @@ -149,6 +149,72 @@ authRouter.post('/login', async (req, res) => { return res.json({ token, user: mapUserRecord(userRecord) }); }); +authRouter.post('/oauth', async (req, res) => { + const { accessToken } = req.body || {}; + if (!accessToken) { + return res.status(400).json({ message: 'Geçerli bir Google oturumu bulunamadı.' }); + } + + try { + const response = await fetch(`${process.env.SUPABASE_URL}/auth/v1/user`, { + headers: { + Authorization: `Bearer ${accessToken}`, + apikey: process.env.SUPABASE_SERVICE_ROLE_KEY, + }, + }); + + if (!response.ok) { + return res.status(401).json({ message: 'Google oturumu doğrulanamadı.' }); + } + + const supabaseUser = await response.json(); + const email = supabaseUser?.email?.toLowerCase(); + if (!email) { + return res.status(400).json({ message: 'Google hesabında email bilgisi bulunamadı.' }); + } + const name = + supabaseUser?.user_metadata?.full_name || + supabaseUser?.user_metadata?.name || + email.split('@')[0]; + const username = (supabaseUser?.user_metadata?.user_name || email.split('@')[0]).replace(/[^a-zA-Z0-9-_]/g, ''); + + const { data: existingUser, error: fetchError } = await supabase + .from(USERS_TABLE) + .select('id,email,name,username') + .eq('email', email) + .maybeSingle(); + + if (fetchError && fetchError.code !== 'PGRST116') { + return res.status(500).json({ message: 'Kullanıcı sorgulanamadı.' }); + } + + let userRecord = existingUser; + if (!userRecord) { + const { data: insertedUser, error: insertError } = await supabase + .from(USERS_TABLE) + .insert({ + name, + email, + username, + password_hash: 'GOOGLE_OAUTH', + }) + .select('id,email,name,username') + .single(); + + if (insertError || !insertedUser) { + return res.status(500).json({ message: 'Google hesabı oluşturulamadı.' }); + } + userRecord = insertedUser; + } + + const token = createToken(userRecord); + return res.json({ token, user: mapUserRecord(userRecord) }); + } catch (error) { + console.error('Google OAuth hatası:', error); + return res.status(500).json({ message: 'Google girişi tamamlanamadı.' }); + } +}); + authRouter.get('/me', authMiddleware, async (req, res) => { const { data: userRecord, error } = await supabase .from(USERS_TABLE) diff --git a/src/App.jsx b/src/App.jsx index c07ed60..3200d3a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -17,7 +17,8 @@ import { } from '@mui/material'; import { useEffect, useMemo, useState } from 'react'; import { useAppStore } from './store/useAppStore'; -import { fetchCurrentUser, logoutUser } from './utils/authApi'; +import { fetchCurrentUser, loginWithGoogle, logoutUser } from './utils/authApi'; +import { supabaseClient } from './lib/supabaseClient'; export const wizardSteps = [ { label: 'Yükle', path: '/' }, @@ -36,6 +37,8 @@ const App = () => { const initializeAuth = useAppStore((state) => state.initializeAuth); const updateCurrentUser = useAppStore((state) => state.updateCurrentUser); const clearAuthSession = useAppStore((state) => state.clearAuthSession); + const setAuthSession = useAppStore((state) => state.setAuthSession); + const setStoreError = useAppStore((state) => state.setError); const currentUser = useAppStore((state) => state.currentUser); const authToken = useAppStore((state) => state.authToken); const [menuAnchor, setMenuAnchor] = useState(null); @@ -65,6 +68,36 @@ const App = () => { syncUser(); }, [authToken, clearAuthSession, updateCurrentUser]); + useEffect(() => { + if (!supabaseClient) return undefined; + + const exchangeGoogleSession = async (session) => { + if (!session || !session.access_token || authToken) return; + try { + const response = await loginWithGoogle(session.access_token); + setAuthSession(response); + } catch (error) { + setStoreError(error.message); + } finally { + await supabaseClient.auth.signOut(); + } + }; + + supabaseClient.auth.getSession().then(({ data }) => { + exchangeGoogleSession(data.session); + }); + + const { data: subscription } = supabaseClient.auth.onAuthStateChange((event, session) => { + if (event === 'SIGNED_IN') { + exchangeGoogleSession(session); + } + }); + + return () => { + subscription.subscription.unsubscribe(); + }; + }, [authToken, setAuthSession, setStoreError]); + const activeStep = useMemo(() => { const foundIndex = wizardSteps.findIndex((step) => step.path === location.pathname); return foundIndex === -1 ? 0 : foundIndex; @@ -95,8 +128,10 @@ const App = () => { { - + { } }; + const handleGoogleSignIn = async () => { + setFormError(null); + setFormMessage(null); + if (!supabaseClient) { + setFormError('Google ile giriş yapılandırılmadı.'); + return; + } + setSubmitting(true); + try { + const { error } = await supabaseClient.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: window.location.origin, + }, + }); + if (error) throw error; + } catch (error) { + setFormError(error.message || 'Google ile giriş başlatılamadı.'); + setSubmitting(false); + } + }; + return ( { background: 'linear-gradient(180deg, #F9F7F4 0%, #F3EEE5 60%, #F9F7F4 100%)', display: 'flex', alignItems: 'center', - py: { xs: 6, md: 10 }, + py: { xs: 2, md: 4 }, }} > - + { overflow: 'hidden', p: 0, display: 'grid', - gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, + gridTemplateColumns: { xs: '1fr', md: '520px 1fr' }, + minHeight: { xs: 'auto', md: 520 }, }} > - - - + + + imagepub {variant.eyebrow} - + {variant.heading} - {variant.body} + + {variant.body} + - + veya e-posta ile - + {formFields.map((field) => ( { ))} {formError && {formError}} {formMessage && {formMessage}} - @@ -252,7 +293,7 @@ const AuthPage = ({ mode }) => { sx={{ backgroundColor: '#F5F1EA', borderLeft: { md: '1px solid #E4DDD3' }, - p: { xs: 4, md: 6 }, + p: { xs: 2.5, md: 4 }, display: 'flex', flexDirection: 'column', justifyContent: 'center', diff --git a/src/utils/authApi.js b/src/utils/authApi.js index e04fa34..98d4629 100644 --- a/src/utils/authApi.js +++ b/src/utils/authApi.js @@ -21,3 +21,9 @@ export const requestPasswordReset = (payload) => method: 'POST', data: payload, }); + +export const loginWithGoogle = (accessToken) => + apiClient('/auth/oauth', { + method: 'POST', + data: { accessToken }, + });