Supabase login/register entegrasyonu
This commit is contained in:
162
server/index.js
162
server/index.js
@@ -1,16 +1,37 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import Epub from 'epub-gen';
|
||||
|
||||
const requiredEnv = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'JWT_SECRET'];
|
||||
requiredEnv.forEach((key) => {
|
||||
if (!process.env[key]) {
|
||||
console.error(`Missing required environment variable: ${key}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 4000;
|
||||
const ORIGIN = process.env.CLIENT_ORIGIN || 'http://localhost:5173';
|
||||
const allowedOrigins = ORIGIN.split(',').map((origin) => origin.trim());
|
||||
|
||||
app.use(cors({ origin: ORIGIN, credentials: true }));
|
||||
const USERS_TABLE = process.env.SUPABASE_USERS_TABLE || 'users';
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
import { supabase } from './src/services/supabaseClient.js';
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
const sanitizeHtml = (text = '') =>
|
||||
@@ -22,6 +43,145 @@ const sanitizeHtml = (text = '') =>
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n/g, '<br/>');
|
||||
|
||||
const createToken = (user) =>
|
||||
jwt.sign(
|
||||
{
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '7d' },
|
||||
);
|
||||
|
||||
const mapUserRecord = (record) => ({
|
||||
id: record.id,
|
||||
email: record.email,
|
||||
name: record.name,
|
||||
username: record.username,
|
||||
});
|
||||
|
||||
const authMiddleware = (req, res, next) => {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Yetkisiz erişim' });
|
||||
}
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
req.user = payload;
|
||||
return next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ message: 'Oturum süresi doldu, lütfen tekrar giriş yap.' });
|
||||
}
|
||||
};
|
||||
|
||||
const authRouter = express.Router();
|
||||
|
||||
authRouter.post('/register', async (req, res) => {
|
||||
const { name, email, password } = req.body || {};
|
||||
if (!name || !email || !password) {
|
||||
return res.status(400).json({ message: 'Ad, email ve şifre gereklidir.' });
|
||||
}
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const username = normalizedEmail.split('@')[0];
|
||||
|
||||
const { data: existingUser, error: existingError } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.select('id')
|
||||
.eq('email', normalizedEmail)
|
||||
.maybeSingle();
|
||||
|
||||
if (existingError && existingError.code !== 'PGRST116') {
|
||||
return res.status(500).json({ message: 'Kullanıcı kontrolü başarısız.' });
|
||||
}
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(409).json({ message: 'Bu email adresi ile zaten bir hesap mevcut.' });
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
const { data: createdUser, error: insertError } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.insert({
|
||||
name: name.trim(),
|
||||
email: normalizedEmail,
|
||||
username,
|
||||
password_hash: passwordHash,
|
||||
})
|
||||
.select('id,email,name,username')
|
||||
.single();
|
||||
|
||||
if (insertError || !createdUser) {
|
||||
return res.status(500).json({ message: 'Kullanıcı oluşturulamadı.' });
|
||||
}
|
||||
|
||||
const token = createToken(createdUser);
|
||||
return res.status(201).json({ token, user: mapUserRecord(createdUser) });
|
||||
});
|
||||
|
||||
authRouter.post('/login', async (req, res) => {
|
||||
const { email, password } = req.body || {};
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ message: 'Email ve şifre gereklidir.' });
|
||||
}
|
||||
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const { data: userRecord, error: fetchError } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.select('id,email,name,username,password_hash')
|
||||
.eq('email', normalizedEmail)
|
||||
.maybeSingle();
|
||||
|
||||
if (fetchError && fetchError.code !== 'PGRST116') {
|
||||
return res.status(500).json({ message: 'Giriş işlemi başarısız.' });
|
||||
}
|
||||
if (!userRecord) {
|
||||
return res.status(401).json({ message: 'Email veya şifre hatalı.' });
|
||||
}
|
||||
|
||||
const validPassword = await bcrypt.compare(password, userRecord.password_hash);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ message: 'Email veya şifre hatalı.' });
|
||||
}
|
||||
|
||||
const token = createToken(userRecord);
|
||||
return res.json({ token, user: mapUserRecord(userRecord) });
|
||||
});
|
||||
|
||||
authRouter.get('/me', authMiddleware, async (req, res) => {
|
||||
const { data: userRecord, error } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.select('id,email,name,username')
|
||||
.eq('id', req.user.sub)
|
||||
.single();
|
||||
|
||||
if (error || !userRecord) {
|
||||
return res.status(404).json({ message: 'Kullanıcı bulunamadı.' });
|
||||
}
|
||||
|
||||
return res.json({ user: mapUserRecord(userRecord) });
|
||||
});
|
||||
|
||||
authRouter.post('/logout', (_req, res) => {
|
||||
return res.json({ message: 'Çıkış yapıldı.' });
|
||||
});
|
||||
|
||||
authRouter.post('/forgot-password', async (req, res) => {
|
||||
const { email } = req.body || {};
|
||||
if (!email) {
|
||||
return res.status(400).json({ message: 'Email gereklidir.' });
|
||||
}
|
||||
|
||||
// Supabase Auth kullanılmadığı için burada sadece bilgilendirici bir cevap döndürüyoruz.
|
||||
return res.json({
|
||||
message:
|
||||
'Şifre sıfırlama talebin alındı. Bu demo ortamında e-posta gönderimi aktif değildir.',
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/auth', authRouter);
|
||||
|
||||
app.post('/generate-epub', async (req, res) => {
|
||||
const { text, meta, cover } = req.body || {};
|
||||
if (!text || !text.trim()) {
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.45.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"epub-gen": "^0.1.0",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemon": "^3.1.4",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
|
||||
11
server/src/services/supabaseClient.js
Normal file
11
server/src/services/supabaseClient.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const requiredEnv = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'];
|
||||
requiredEnv.forEach((key) => {
|
||||
if (!process.env[key]) {
|
||||
console.error(`Missing required Supabase env var: ${key}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
export const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
|
||||
137
src/App.jsx
137
src/App.jsx
@@ -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'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}>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 aç
|
||||
Ş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}
|
||||
|
||||
5
src/pages/auth/ForgotPassword.jsx
Normal file
5
src/pages/auth/ForgotPassword.jsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import AuthPage from './AuthPage';
|
||||
|
||||
const ForgotPassword = () => <AuthPage mode="forgot" />;
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -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
27
src/utils/apiClient.js
Normal 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
23
src/utils/authApi.js
Normal 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,
|
||||
});
|
||||
Reference in New Issue
Block a user