google login eklendi

This commit is contained in:
2025-11-13 21:44:08 +03:00
parent 73ad410535
commit cd0b59945b
7 changed files with 182 additions and 18 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
VITE_API_BASE_URL=http://localhost:4000
VITE_SUPABASE_URL=""
VITE_SUPABASE_ANON_KEY=""

View File

@@ -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",

View File

@@ -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)

View File

@@ -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 = () => {
<Box
component="header"
sx={{
position: 'sticky',
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 1200,
backgroundColor: '#FFFFFF',
borderBottom: '1px solid #E0DFDC',
@@ -178,7 +213,7 @@ const App = () => {
</Stack>
</Container>
</Box>
<Container maxWidth="lg" sx={{ py: 6 }}>
<Container maxWidth="lg" sx={{ pt: 14, pb: 6 }}>
<Box mb={5} textAlign="center">
<Typography
variant="h3"

12
src/lib/supabaseClient.js Normal file
View File

@@ -0,0 +1,12 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
console.warn('Supabase environment variables are not set. Google auth is disabled.');
}
export const supabaseClient = supabaseUrl && supabaseAnonKey
? createClient(supabaseUrl, supabaseAnonKey)
: null;

View File

@@ -14,6 +14,7 @@ import {
import { Link as RouterLink, useNavigate } from 'react-router-dom';
import { useAppStore } from '../../store/useAppStore';
import { loginUser, registerUser, requestPasswordReset } from '../../utils/authApi';
import { supabaseClient } from '../../lib/supabaseClient';
const copy = {
login: {
@@ -140,6 +141,28 @@ const AuthPage = ({ mode }) => {
}
};
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 (
<Box
sx={{
@@ -147,10 +170,10 @@ const AuthPage = ({ mode }) => {
background: 'linear-gradient(180deg, #F9F7F4 0%, #F3EEE5 60%, #F9F7F4 100%)',
display: 'flex',
alignItems: 'center',
py: { xs: 6, md: 10 },
py: { xs: 2, md: 4 },
}}
>
<Container maxWidth="lg">
<Container maxWidth="lg" sx={{ px: { xs: 2, md: 6 } }}>
<Paper
elevation={0}
sx={{
@@ -158,39 +181,57 @@ const AuthPage = ({ mode }) => {
overflow: 'hidden',
p: 0,
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
gridTemplateColumns: { xs: '1fr', md: '520px 1fr' },
minHeight: { xs: 'auto', md: 520 },
}}
>
<Box sx={{ p: { xs: 4, md: 6 } }}>
<Stack spacing={3} maxWidth={420} mx="auto">
<Stack spacing={1}>
<Box sx={{ p: { xs: 2.5, md: 4 } }}>
<Stack spacing={2} maxWidth={{ xs: '100%', md: 360 }} mx="auto">
<Stack spacing={0.75}>
<Typography
variant="h4"
sx={{ fontFamily: '"Caudex", serif', color: '#1C1815', fontWeight: 700, letterSpacing: 1 }}
sx={{
fontFamily: '"Caudex", serif',
color: '#1C1815',
fontWeight: 700,
letterSpacing: 1,
fontSize: { xs: '1.35rem', md: '1.6rem' },
}}
>
imagepub
</Typography>
<Typography variant="overline" sx={{ color: '#B5AD9A', letterSpacing: 2 }}>
{variant.eyebrow}
</Typography>
<Typography variant="h4" sx={{ color: '#1C1815', fontWeight: 700, lineHeight: 1.2 }}>
<Typography
variant="h4"
sx={{
color: '#1C1815',
fontWeight: 700,
lineHeight: 1.2,
fontSize: { xs: '1.5rem', md: '1.9rem' },
}}
>
{variant.heading}
</Typography>
<Typography sx={{ color: '#5A5751', lineHeight: 1.6 }}>{variant.body}</Typography>
<Typography sx={{ color: '#5A5751', lineHeight: 1.4, fontSize: { xs: '0.92rem', md: '1rem' } }}>
{variant.body}
</Typography>
</Stack>
<Stack spacing={2}>
<Stack spacing={1.5}>
<Button
fullWidth
size="large"
variant="outlined"
disabled
startIcon={<Box component="img" src="/google.svg" alt="Google" width={18} height={18} />}
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}
</Button>
<Divider sx={{ color: '#B5AD9A', fontSize: 12 }}>veya e-posta ile</Divider>
<Stack spacing={2} component="form" onSubmit={handleSubmit}>
<Stack spacing={1.5} component="form" onSubmit={handleSubmit}>
{formFields.map((field) => (
<TextField
key={field.name}
@@ -223,7 +264,7 @@ const AuthPage = ({ mode }) => {
))}
{formError && <Alert severity="error">{formError}</Alert>}
{formMessage && <Alert severity="success">{formMessage}</Alert>}
<Button type="submit" variant="contained" size="large" fullWidth sx={{ py: 1.4 }} disabled={submitting}>
<Button type="submit" variant="contained" size="large" fullWidth sx={{ py: 1 }} disabled={submitting}>
{variant.submit}
</Button>
</Stack>
@@ -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',

View File

@@ -21,3 +21,9 @@ export const requestPasswordReset = (payload) =>
method: 'POST',
data: payload,
});
export const loginWithGoogle = (accessToken) =>
apiClient('/auth/oauth', {
method: 'POST',
data: { accessToken },
});