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}
+
-
+
}
+ onClick={handleGoogleSignIn}
+ disabled={submitting || !supabaseClient}
sx={{
borderColor: '#E0DFDC',
color: '#1C1815',
backgroundColor: '#FFFFFF',
borderRadius: 3,
- py: 1.5,
+ py: 1.6,
fontWeight: 600,
'&:hover': { borderColor: '#CFC8BD', backgroundColor: '#FAF8F6' },
'&.Mui-disabled': {
@@ -205,7 +246,7 @@ const AuthPage = ({ mode }) => {
{variant.google}
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 },
+ });