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",
|
"@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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
41
src/App.jsx
41
src/App.jsx
@@ -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
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 { 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',
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user