Supabase login/register entegrasyonu

This commit is contained in:
2025-11-12 22:23:47 +03:00
parent 7d2d9a54e9
commit 73ad410535
10 changed files with 502 additions and 49 deletions

View File

@@ -5,6 +5,8 @@ import {
Button,
Container,
Link,
Menu,
MenuItem,
Paper,
Snackbar,
Stack,
@@ -13,8 +15,9 @@ import {
Stepper,
Typography,
} from '@mui/material';
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useAppStore } from './store/useAppStore';
import { fetchCurrentUser, logoutUser } from './utils/authApi';
export const wizardSteps = [
{ label: 'Yükle', path: '/' },
@@ -30,17 +33,63 @@ const App = () => {
const navigate = useNavigate();
const error = useAppStore((state) => state.error);
const clearError = useAppStore((state) => state.clearError);
const initializeAuth = useAppStore((state) => state.initializeAuth);
const updateCurrentUser = useAppStore((state) => state.updateCurrentUser);
const clearAuthSession = useAppStore((state) => state.clearAuthSession);
const currentUser = useAppStore((state) => state.currentUser);
const authToken = useAppStore((state) => state.authToken);
const [menuAnchor, setMenuAnchor] = useState(null);
const handleSnackbarClose = (_, reason) => {
if (reason === 'clickaway') return;
clearError();
};
useEffect(() => {
initializeAuth();
}, [initializeAuth]);
useEffect(() => {
const syncUser = async () => {
if (!authToken) return;
try {
const response = await fetchCurrentUser(authToken);
if (response?.user) {
updateCurrentUser(response.user);
}
} catch (error) {
console.warn('Oturum doğrulanamadı:', error.message);
clearAuthSession();
}
};
syncUser();
}, [authToken, clearAuthSession, updateCurrentUser]);
const activeStep = useMemo(() => {
const foundIndex = wizardSteps.findIndex((step) => step.path === location.pathname);
return foundIndex === -1 ? 0 : foundIndex;
}, [location.pathname]);
const handleMenuOpen = (event) => {
setMenuAnchor(event.currentTarget);
};
const handleMenuClose = () => setMenuAnchor(null);
const handleLogout = async () => {
try {
if (authToken) {
await logoutUser(authToken);
}
} catch (error) {
console.warn(ıkış işlemi sırasında hata:', error.message);
} finally {
clearAuthSession();
handleMenuClose();
navigate('/');
}
};
return (
<>
<Box
@@ -55,7 +104,7 @@ const App = () => {
}}
>
<Container maxWidth="lg">
<Stack direction="row" alignItems="center" justifyContent="space-between" py={0.5}>
<Stack direction="row" alignItems="center" justifyContent="space-between" py={1.5}>
<Typography
variant="h4"
sx={{
@@ -68,28 +117,63 @@ const App = () => {
imagepub
</Typography>
<Stack direction="row" spacing={3} alignItems="center">
<Typography
component="button"
type="button"
onClick={() => navigate('/login')}
sx={{
background: 'none',
border: 'none',
color: '#2F2D28',
fontWeight: 600,
fontSize: '0.95rem',
cursor: 'pointer',
textTransform: 'none',
letterSpacing: 0.5,
transition: 'color 0.2s ease',
'&:hover': { color: '#d7b16a' },
}}
>
Login
</Typography>
<Button variant="contained" color="primary" size="small" onClick={() => navigate('/register')}>
Register
</Button>
{currentUser ? (
<>
<Typography
component="button"
type="button"
onClick={handleMenuOpen}
sx={{
background: 'none',
border: 'none',
color: '#2F2D28',
fontWeight: 600,
fontSize: '0.95rem',
cursor: 'pointer',
textTransform: 'none',
letterSpacing: 0.5,
display: 'flex',
alignItems: 'center',
gap: 1,
transition: 'color 0.2s ease',
'&:hover': { color: '#d7b16a' },
}}
>
{currentUser.username}
<Box component="i" className="fa-solid fa-chevron-down" sx={{ fontSize: '0.85rem', color: 'inherit' }} />
</Typography>
<Menu anchorEl={menuAnchor} open={Boolean(menuAnchor)} onClose={handleMenuClose}>
<MenuItem disabled>Hesap Ayarlarım</MenuItem>
<MenuItem disabled>EPUB'larım</MenuItem>
<MenuItem onClick={handleLogout}>Çıkış</MenuItem>
</Menu>
</>
) : (
<>
<Typography
component="button"
type="button"
onClick={() => navigate('/login')}
sx={{
background: 'none',
border: 'none',
color: '#2F2D28',
fontWeight: 600,
fontSize: '0.95rem',
cursor: 'pointer',
textTransform: 'none',
letterSpacing: 0.5,
transition: 'color 0.2s ease',
'&:hover': { color: '#d7b16a' },
}}
>
Login
</Typography>
<Button variant="contained" color="primary" size="small" onClick={() => navigate('/register')}>
Register
</Button>
</>
)}
</Stack>
</Stack>
</Container>
@@ -105,7 +189,7 @@ const App = () => {
fontSize: { xs: '1.9rem', md: '2.6rem' },
}}
>
Tek ekranda görselden EPUB&apos;a kadar tüm sihirbaz adımları
Tek ekranda görselden EPUB'a kadar tüm sihirbaz adımları
</Typography>
<Typography
sx={{
@@ -117,8 +201,7 @@ const App = () => {
mx: 'auto',
}}
>
Kapak seç, crop alanını belirle, OCR ile Türkçe metinleri koru ve sonucunu tek tıkla EPUB olarak indir.
imgpub tüm işlemleri tek bir akışta toplar, hızlı ve modern bir deneyim sunar.
Kapak seç, crop alanını belirle, OCR ile Türkçe metinleri koru ve sonucunu tek tıkla EPUB olarak indir. imgpub tüm işlemleri tek bir akışta toplar, hızlı ve modern bir deneyim sunar.
</Typography>
</Box>
<Paper sx={{ p: { xs: 2, md: 4 }, mb: 4 }} elevation={0}>

View File

@@ -11,6 +11,7 @@ import EpubStep from './components/EpubStep';
import DownloadStep from './components/DownloadStep';
import Login from './pages/auth/Login';
import Register from './pages/auth/Register';
import ForgotPassword from './pages/auth/ForgotPassword';
const theme = createTheme({
palette: {
@@ -66,7 +67,7 @@ const theme = createTheme({
root: {
backgroundColor: '#FFFFFF',
border: '1px solid #D2D2D2',
borderRadius: '1.5px',
borderRadius: '16px',
boxShadow: '0 10px 18px rgba(0,0,0,0.06)',
},
},
@@ -109,7 +110,7 @@ const theme = createTheme({
label: {
backgroundColor: '#FFFFFF',
border: '1px solid #D2D2D2',
borderRadius: '1.5px',
borderRadius: '16px',
padding: '4px 10px',
color: '#1C1815',
boxShadow: '0 6px 12px rgba(0,0,0,0.05)',
@@ -139,6 +140,7 @@ const router = createBrowserRouter([
},
{ path: '/login', element: <Login /> },
{ path: '/register', element: <Register /> },
{ path: '/forgot-password', element: <ForgotPassword /> },
]);
ReactDOM.createRoot(document.getElementById('root')).render(

View File

@@ -1,4 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import {
Alert,
Box,
Button,
Container,
@@ -9,7 +11,9 @@ import {
TextField,
Typography,
} from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
import { useAppStore } from '../../store/useAppStore';
import { loginUser, registerUser, requestPasswordReset } from '../../utils/authApi';
const copy = {
login: {
@@ -42,6 +46,21 @@ const copy = {
'Kapak ve önizleme görsellerini tek tuşla üret',
],
},
forgot: {
eyebrow: 'Sıfırlama bağlantısı gönder',
heading: 'Şifreni mi unuttun?',
body:
'imgpub hesabına tekrar erişmek için email adresini gir. Demo ortamında e-posta gönderimi simüle edilir.',
submit: 'Sıfırlama bağlantısı gönder',
switchLabel: 'Parolanı hatırladın mı?',
switchLink: { to: '/login', label: 'Giriş yap' },
google: 'Google ile devam et',
highlights: [
'Akışını kaybetmeden geri dön',
'Hızlıca yeni parola belirle',
'Güvenli studio erişimini koru',
],
},
};
const fields = {
@@ -54,11 +73,72 @@ const fields = {
{ name: 'email', label: 'Email adresi', type: 'email' },
{ name: 'password', label: 'Şifre', type: 'password' },
],
forgot: [{ name: 'email', label: 'Email adresi', type: 'email' }],
};
const highlightIconMap = {
'Kütüphanendeki projeleri bulutta sakla': 'fa-solid fa-crop-simple',
'OCR sonrası metinleri EPUB şablonlarına aktar': 'fa-solid fa-book',
'Toplu crop ile sayfa düzenini koru': 'fa-solid fa-image',
'Akıllı crop önerileri ile zamandan kazan': 'fa-solid fa-crop-simple',
'OCR çıktısını EPUB ve PDF olarak taşı': 'fa-solid fa-book',
'Kapak ve önizleme görsellerini tek tuşla üret': 'fa-solid fa-image',
'Akışını kaybetmeden geri dön': 'fa-solid fa-rotate-left',
'Hızlıca yeni parola belirle': 'fa-solid fa-key',
'Güvenli studio erişimini koru': 'fa-solid fa-lock',
};
const AuthPage = ({ mode }) => {
const variant = copy[mode];
const formFields = fields[mode];
const formFields = fields[mode] || [];
const navigate = useNavigate();
const setAuthSession = useAppStore((state) => state.setAuthSession);
const [formError, setFormError] = useState(null);
const [formMessage, setFormMessage] = useState(null);
const [submitting, setSubmitting] = useState(false);
const defaultValues = useMemo(
() => formFields.reduce((acc, field) => ({ ...acc, [field.name]: '' }), {}),
[formFields],
);
const [formValues, setFormValues] = useState(defaultValues);
useEffect(() => {
setFormValues(defaultValues);
setFormError(null);
setFormMessage(null);
}, [defaultValues]);
const handleChange = (event) => {
const { name, value } = event.target;
setFormValues((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (event) => {
event.preventDefault();
setFormError(null);
setFormMessage(null);
setSubmitting(true);
try {
if (mode === 'login') {
const response = await loginUser(formValues);
setAuthSession(response);
navigate('/');
} else if (mode === 'register') {
const response = await registerUser(formValues);
setAuthSession(response);
navigate('/');
} else if (mode === 'forgot') {
await requestPasswordReset({ email: formValues.email });
setFormMessage('Şifre sıfırlama talebin alındı. Lütfen e-posta kutunu kontrol et.');
}
} catch (error) {
setFormError(error.message);
} finally {
setSubmitting(false);
}
};
return (
<Box
@@ -74,7 +154,7 @@ const AuthPage = ({ mode }) => {
<Paper
elevation={0}
sx={{
borderRadius: 5,
borderRadius: 1,
overflow: 'hidden',
p: 0,
display: 'grid',
@@ -103,6 +183,7 @@ const AuthPage = ({ mode }) => {
fullWidth
size="large"
variant="outlined"
disabled
startIcon={<Box component="img" src="/google.svg" alt="Google" width={18} height={18} />}
sx={{
borderColor: '#E0DFDC',
@@ -112,12 +193,19 @@ const AuthPage = ({ mode }) => {
py: 1.5,
fontWeight: 600,
'&:hover': { borderColor: '#CFC8BD', backgroundColor: '#FAF8F6' },
'&.Mui-disabled': {
opacity: 1,
borderColor: '#E0DFDC',
color: '#1C1815',
backgroundColor: '#FFFFFF',
cursor: 'not-allowed',
},
}}
>
{variant.google}
</Button>
<Divider sx={{ color: '#B5AD9A', fontSize: 12 }}>veya e-posta ile</Divider>
<Stack spacing={2} component="form">
<Stack spacing={2} component="form" onSubmit={handleSubmit}>
{formFields.map((field) => (
<TextField
key={field.name}
@@ -125,30 +213,38 @@ const AuthPage = ({ mode }) => {
label={field.label}
name={field.name}
fullWidth
required
value={formValues[field.name] || ''}
onChange={handleChange}
variant="outlined"
InputProps={{ sx: { borderRadius: 3 } }}
disabled={submitting}
/>
))}
<Button type="submit" variant="contained" size="large" fullWidth sx={{ py: 1.4 }}>
{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}>
{variant.submit}
</Button>
</Stack>
{mode === 'login' && (
<Link
component={RouterLink}
to="/register"
to="/forgot-password"
underline="none"
sx={{ color: '#C0462A', fontWeight: 600, fontSize: 14, textAlign: 'center' }}
>
Şifreni mi unuttun? Yeni hesap
Şifreni mi unuttun?
</Link>
)}
<Typography sx={{ color: '#7B7366', fontSize: 14, textAlign: 'center' }}>
{variant.switchLabel}{' '}
<Link component={RouterLink} to={variant.switchLink.to} underline="none" sx={{ fontWeight: 600 }}>
{variant.switchLink.label}
</Link>
</Typography>
{variant.switchLabel && variant.switchLink && (
<Typography sx={{ color: '#7B7366', fontSize: 14, textAlign: 'center' }}>
{variant.switchLabel}{' '}
<Link component={RouterLink} to={variant.switchLink.to} underline="none" sx={{ fontWeight: 600 }}>
{variant.switchLink.label}
</Link>
</Typography>
)}
</Stack>
</Stack>
</Box>
@@ -189,12 +285,7 @@ const AuthShowcase = ({ mode }) => {
<AuthIllustration />
<Stack spacing={1.5}>
{variant.highlights.map((item) => {
const iconMap = {
'Kütüphanendeki projeleri bulutta sakla': 'fa-solid fa-crop-simple',
'OCR sonrası metinleri EPUB şablonlarına aktar': 'fa-solid fa-book',
'Toplu crop ile sayfa düzenini koru': 'fa-solid fa-image',
};
const iconClass = iconMap[item];
const iconClass = highlightIconMap[item];
return (
<Stack
key={item}

View File

@@ -0,0 +1,5 @@
import AuthPage from './AuthPage';
const ForgotPassword = () => <AuthPage mode="forgot" />;
export default ForgotPassword;

View File

@@ -1,5 +1,23 @@
import { create } from 'zustand';
const TOKEN_STORAGE_KEY = 'imgpub_token';
const USER_STORAGE_KEY = 'imgpub_user';
const readStoredAuth = () => {
if (typeof window === 'undefined') {
return { token: null, user: null };
}
try {
const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
const userRaw = window.localStorage.getItem(USER_STORAGE_KEY);
const user = userRaw ? JSON.parse(userRaw) : null;
return { token, user };
} catch (error) {
console.warn('Stored auth okunamadı', error);
return { token: null, user: null };
}
};
const createEmptyCropConfig = () => ({
x: 0,
y: 0,
@@ -27,6 +45,8 @@ export const useAppStore = create((set) => ({
croppedCoverImage: null,
ocrText: '',
generatedEpub: null,
authToken: null,
currentUser: null,
error: null,
setError: (message) => set({ error: message }),
clearError: () => set({ error: null }),
@@ -55,6 +75,33 @@ export const useAppStore = create((set) => ({
}
return { generatedEpub: epub };
}),
initializeAuth: () => {
const { token, user } = readStoredAuth();
if (token && user) {
set({ authToken: token, currentUser: user });
}
},
setAuthSession: ({ token, user }) => {
if (typeof window !== 'undefined') {
window.localStorage.setItem(TOKEN_STORAGE_KEY, token);
window.localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
}
set({ authToken: token, currentUser: user });
},
updateCurrentUser: (user) =>
set((state) => {
if (typeof window !== 'undefined' && state.authToken) {
window.localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
}
return { currentUser: user };
}),
clearAuthSession: () => {
if (typeof window !== 'undefined') {
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
window.localStorage.removeItem(USER_STORAGE_KEY);
}
set({ authToken: null, currentUser: null });
},
setCoverImageId: (id) =>
set((state) => {
const draft = {

27
src/utils/apiClient.js Normal file
View File

@@ -0,0 +1,27 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:4000';
const handleResponse = async (response) => {
const contentType = response.headers.get('content-type');
const isJson = contentType && contentType.includes('application/json');
const data = isJson ? await response.json() : await response.text();
if (!response.ok) {
const message = (isJson && data?.message) || 'Beklenmeyen bir hata oluştu.';
throw new Error(message);
}
return data;
};
export const apiClient = async (path, { method = 'GET', data, token } = {}) => {
const headers = { 'Content-Type': 'application/json' };
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}${path}`, {
method,
headers,
body: data ? JSON.stringify(data) : undefined,
});
return handleResponse(response);
};

23
src/utils/authApi.js Normal file
View File

@@ -0,0 +1,23 @@
import { apiClient } from './apiClient';
export const registerUser = (payload) => apiClient('/auth/register', { method: 'POST', data: payload });
export const loginUser = (payload) => apiClient('/auth/login', { method: 'POST', data: payload });
export const fetchCurrentUser = (token) =>
apiClient('/auth/me', {
method: 'GET',
token,
});
export const logoutUser = (token) =>
apiClient('/auth/logout', {
method: 'POST',
token,
});
export const requestPasswordReset = (payload) =>
apiClient('/auth/forgot-password', {
method: 'POST',
data: payload,
});