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", "@emotion/styled": "^11.13.5",
"@mui/icons-material": "^6.1.1", "@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1", "@mui/material": "^6.1.1",
"@supabase/supabase-js": "^2.81.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",

View File

@@ -149,6 +149,72 @@ authRouter.post('/login', async (req, res) => {
return res.json({ token, user: mapUserRecord(userRecord) }); 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) => { authRouter.get('/me', authMiddleware, async (req, res) => {
const { data: userRecord, error } = await supabase const { data: userRecord, error } = await supabase
.from(USERS_TABLE) .from(USERS_TABLE)

View File

@@ -17,7 +17,8 @@ import {
} from '@mui/material'; } from '@mui/material';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useAppStore } from './store/useAppStore'; 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 = [ export const wizardSteps = [
{ label: 'Yükle', path: '/' }, { label: 'Yükle', path: '/' },
@@ -36,6 +37,8 @@ const App = () => {
const initializeAuth = useAppStore((state) => state.initializeAuth); const initializeAuth = useAppStore((state) => state.initializeAuth);
const updateCurrentUser = useAppStore((state) => state.updateCurrentUser); const updateCurrentUser = useAppStore((state) => state.updateCurrentUser);
const clearAuthSession = useAppStore((state) => state.clearAuthSession); 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 currentUser = useAppStore((state) => state.currentUser);
const authToken = useAppStore((state) => state.authToken); const authToken = useAppStore((state) => state.authToken);
const [menuAnchor, setMenuAnchor] = useState(null); const [menuAnchor, setMenuAnchor] = useState(null);
@@ -65,6 +68,36 @@ const App = () => {
syncUser(); syncUser();
}, [authToken, clearAuthSession, updateCurrentUser]); }, [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 activeStep = useMemo(() => {
const foundIndex = wizardSteps.findIndex((step) => step.path === location.pathname); const foundIndex = wizardSteps.findIndex((step) => step.path === location.pathname);
return foundIndex === -1 ? 0 : foundIndex; return foundIndex === -1 ? 0 : foundIndex;
@@ -95,8 +128,10 @@ const App = () => {
<Box <Box
component="header" component="header"
sx={{ sx={{
position: 'sticky', position: 'fixed',
top: 0, top: 0,
left: 0,
right: 0,
zIndex: 1200, zIndex: 1200,
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderBottom: '1px solid #E0DFDC', borderBottom: '1px solid #E0DFDC',
@@ -178,7 +213,7 @@ const App = () => {
</Stack> </Stack>
</Container> </Container>
</Box> </Box>
<Container maxWidth="lg" sx={{ py: 6 }}> <Container maxWidth="lg" sx={{ pt: 14, pb: 6 }}>
<Box mb={5} textAlign="center"> <Box mb={5} textAlign="center">
<Typography <Typography
variant="h3" 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 { Link as RouterLink, useNavigate } from 'react-router-dom';
import { useAppStore } from '../../store/useAppStore'; import { useAppStore } from '../../store/useAppStore';
import { loginUser, registerUser, requestPasswordReset } from '../../utils/authApi'; import { loginUser, registerUser, requestPasswordReset } from '../../utils/authApi';
import { supabaseClient } from '../../lib/supabaseClient';
const copy = { const copy = {
login: { 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 ( return (
<Box <Box
sx={{ sx={{
@@ -147,10 +170,10 @@ const AuthPage = ({ mode }) => {
background: 'linear-gradient(180deg, #F9F7F4 0%, #F3EEE5 60%, #F9F7F4 100%)', background: 'linear-gradient(180deg, #F9F7F4 0%, #F3EEE5 60%, #F9F7F4 100%)',
display: 'flex', display: 'flex',
alignItems: 'center', 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 <Paper
elevation={0} elevation={0}
sx={{ sx={{
@@ -158,39 +181,57 @@ const AuthPage = ({ mode }) => {
overflow: 'hidden', overflow: 'hidden',
p: 0, p: 0,
display: 'grid', 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 } }}> <Box sx={{ p: { xs: 2.5, md: 4 } }}>
<Stack spacing={3} maxWidth={420} mx="auto"> <Stack spacing={2} maxWidth={{ xs: '100%', md: 360 }} mx="auto">
<Stack spacing={1}> <Stack spacing={0.75}>
<Typography <Typography
variant="h4" 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 imagepub
</Typography> </Typography>
<Typography variant="overline" sx={{ color: '#B5AD9A', letterSpacing: 2 }}> <Typography variant="overline" sx={{ color: '#B5AD9A', letterSpacing: 2 }}>
{variant.eyebrow} {variant.eyebrow}
</Typography> </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} {variant.heading}
</Typography> </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>
<Stack spacing={2}> <Stack spacing={1.5}>
<Button <Button
fullWidth fullWidth
size="large" size="large"
variant="outlined" variant="outlined"
disabled
startIcon={<Box component="img" src="/google.svg" alt="Google" width={18} height={18} />} startIcon={<Box component="img" src="/google.svg" alt="Google" width={18} height={18} />}
onClick={handleGoogleSignIn}
disabled={submitting || !supabaseClient}
sx={{ sx={{
borderColor: '#E0DFDC', borderColor: '#E0DFDC',
color: '#1C1815', color: '#1C1815',
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderRadius: 3, borderRadius: 3,
py: 1.5, py: 1.6,
fontWeight: 600, fontWeight: 600,
'&:hover': { borderColor: '#CFC8BD', backgroundColor: '#FAF8F6' }, '&:hover': { borderColor: '#CFC8BD', backgroundColor: '#FAF8F6' },
'&.Mui-disabled': { '&.Mui-disabled': {
@@ -205,7 +246,7 @@ const AuthPage = ({ mode }) => {
{variant.google} {variant.google}
</Button> </Button>
<Divider sx={{ color: '#B5AD9A', fontSize: 12 }}>veya e-posta ile</Divider> <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) => ( {formFields.map((field) => (
<TextField <TextField
key={field.name} key={field.name}
@@ -223,7 +264,7 @@ const AuthPage = ({ mode }) => {
))} ))}
{formError && <Alert severity="error">{formError}</Alert>} {formError && <Alert severity="error">{formError}</Alert>}
{formMessage && <Alert severity="success">{formMessage}</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} {variant.submit}
</Button> </Button>
</Stack> </Stack>
@@ -252,7 +293,7 @@ const AuthPage = ({ mode }) => {
sx={{ sx={{
backgroundColor: '#F5F1EA', backgroundColor: '#F5F1EA',
borderLeft: { md: '1px solid #E4DDD3' }, borderLeft: { md: '1px solid #E4DDD3' },
p: { xs: 4, md: 6 }, p: { xs: 2.5, md: 4 },
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',

View File

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