mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
implemented one-click email verification flow
This commit is contained in:
1 parent
2bf68674ff
commit
1f8823fbda
4 files changed
+109
-2
No files matched your search
@@ -307,10 +307,12 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
email_confirmation_code=code
|
||||
)
|
||||
|
||||
verification_link = f"{settings.FRONTEND_URL}/verify-email?username={username}&code={code}"
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
'Verify your Holy Flavors account',
|
||||
f'Hi {username},\n\nYour verification code is: {code}',
|
||||
f'Hi {username},\n\nYour verification code is: {code}\n\nAlternatively, you can complete your registration by clicking the link below:\n{verification_link}\n\nWelcome to the archive!',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[email],
|
||||
fail_silently=False,
|
||||
@@ -342,10 +344,12 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
user.email_confirmation_code = code
|
||||
user.save()
|
||||
|
||||
verification_link = f"{settings.FRONTEND_URL}/verify-email?username={user.username}&code={code}"
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
'Verify your Holy Flavors account',
|
||||
f'Hi {user.username},\n\nYour new verification code is: {code}',
|
||||
f'Hi {user.username},\n\nYour new verification code is: {code}\n\nAlternatively, you can complete your registration by clicking the link below:\n{verification_link}',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
|
||||
@@ -191,6 +191,8 @@ REST_FRAMEWORK = {
|
||||
CORS_ALLOWED_ORIGINS_STRING = os.environ.get('CORS_ALLOWED_ORIGINS', 'https://holy.narl.io,http://localhost:5173,http://127.0.0.1:5173')
|
||||
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in CORS_ALLOWED_ORIGINS_STRING.split(',')]
|
||||
|
||||
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'https://holy.narl.io').rstrip('/')
|
||||
|
||||
import os
|
||||
# ... (near top of file)
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const Privacy = lazy(() => import('./pages/Privacy'));
|
||||
const Login = lazy(() => import('./pages/Login'));
|
||||
const Settings = lazy(() => import('./pages/Settings'));
|
||||
const Support = lazy(() => import('./pages/Support'));
|
||||
const VerifyEmail = lazy(() => import('./pages/VerifyEmail'));
|
||||
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
|
||||
const AdminUserDetail = lazy(() => import('./pages/AdminUserDetail'));
|
||||
const AdminRatingDetail = lazy(() => import('./pages/AdminRatingDetail'));
|
||||
@@ -696,6 +697,7 @@ const App: React.FC = () => {
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
|
||||
<Route path="/support" element={<Support />} />
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/admin-panel" element={<AdminDashboard />} />
|
||||
<Route path="/admin-panel/user/:id" element={<AdminUserDetail />} />
|
||||
<Route path="/admin-panel/rating/:id" element={<AdminRatingDetail />} />
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate, Link } from 'react-router-dom';
|
||||
import { Container, Paper, Typography, Box, CircularProgress, Button, Alert } from '@mui/material';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
|
||||
const VerifyEmail: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
useTitle(t('auth.verifyTitle'));
|
||||
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(location.search);
|
||||
const username = query.get('username');
|
||||
const code = query.get('code');
|
||||
|
||||
if (!username || !code) {
|
||||
setStatus('error');
|
||||
setErrorMsg('Invalid verification link.');
|
||||
return;
|
||||
}
|
||||
|
||||
const verify = async () => {
|
||||
try {
|
||||
await api.post('users/verify_signup/', { username, code });
|
||||
setStatus('success');
|
||||
// Optional: Auto redirect after 3 seconds
|
||||
setTimeout(() => {
|
||||
navigate('/login');
|
||||
}, 3000);
|
||||
} catch (err: any) {
|
||||
setStatus('error');
|
||||
setErrorMsg(err.response?.data?.error || 'Verification failed.');
|
||||
}
|
||||
};
|
||||
|
||||
verify();
|
||||
}, [location, navigate]);
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ py: 12 }}>
|
||||
<Paper sx={{ p: 6, textAlign: 'center', borderRadius: 4 }}>
|
||||
{status === 'loading' && (
|
||||
<Box>
|
||||
<CircularProgress size={60} sx={{ mb: 4 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>
|
||||
Verifying your account...
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mt: 1 }}>
|
||||
Please wait while we confirm your email.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<Box>
|
||||
<CheckCircleOutlineIcon color="success" sx={{ fontSize: 80, mb: 2 }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 2 }}>
|
||||
{t('auth.verifySuccess')}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mb: 4 }}>
|
||||
You will be redirected to the login page shortly.
|
||||
</Typography>
|
||||
<Button variant="contained" component={Link} to="/login" size="large" sx={{ borderRadius: 2 }}>
|
||||
Go to Login Now
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<Box>
|
||||
<ErrorOutlineIcon color="error" sx={{ fontSize: 80, mb: 2 }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 2 }}>
|
||||
Verification Failed
|
||||
</Typography>
|
||||
<Alert severity="error" sx={{ mb: 4, textAlign: 'left' }}>
|
||||
{errorMsg}
|
||||
</Alert>
|
||||
<Typography color="text.secondary" sx={{ mb: 4 }}>
|
||||
The link might be expired or already used.
|
||||
</Typography>
|
||||
<Button variant="outlined" component={Link} to="/login" size="large" sx={{ borderRadius: 2 }}>
|
||||
Back to Login / Signup
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmail;
|
||||
Reference in new issue
Block a user