google login eklendi
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
VITE_API_BASE_URL=http://localhost:4000
|
||||
VITE_SUPABASE_URL=""
|
||||
VITE_SUPABASE_ANON_KEY=""
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
41
src/App.jsx
41
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 = () => {
|
||||
<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
12
src/lib/supabaseClient.js
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -21,3 +21,9 @@ export const requestPasswordReset = (payload) =>
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
});
|
||||
|
||||
export const loginWithGoogle = (accessToken) =>
|
||||
apiClient('/auth/oauth', {
|
||||
method: 'POST',
|
||||
data: { accessToken },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user