email verfication after account creation, password component, added password special characters
This commit is contained in:
@@ -7,6 +7,7 @@ import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import GoogleCallback from './pages/GoogleCallback';
|
||||
import VerifyEmail from './pages/VerifyEmail';
|
||||
import ItemList from './pages/ItemList';
|
||||
import ItemDetail from './pages/ItemDetail';
|
||||
import EditItem from './pages/EditItem';
|
||||
@@ -38,6 +39,7 @@ function App() {
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/auth/google/callback" element={<GoogleCallback />} />
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/items" element={<ItemList />} />
|
||||
<Route path="/items/:id" element={<ItemDetail />} />
|
||||
<Route path="/users/:id" element={<PublicProfile />} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import PasswordStrengthMeter from "./PasswordStrengthMeter";
|
||||
import PasswordInput from "./PasswordInput";
|
||||
|
||||
interface AuthModalProps {
|
||||
show: boolean;
|
||||
@@ -154,19 +155,18 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
{mode === "signup" && (
|
||||
<PasswordInput
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
{mode === "signup" && (
|
||||
<div style={{ marginTop: '-0.75rem', marginBottom: '1rem' }}>
|
||||
<PasswordStrengthMeter password={password} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
58
frontend/src/components/PasswordInput.tsx
Normal file
58
frontend/src/components/PasswordInput.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface PasswordInputProps {
|
||||
id: string;
|
||||
name?: string;
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const PasswordInput: React.FC<PasswordInputProps> = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
required = false,
|
||||
placeholder,
|
||||
className = 'form-control',
|
||||
label
|
||||
}) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
{label && (
|
||||
<label htmlFor={id} className="form-label">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="position-relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className={className}
|
||||
id={id}
|
||||
name={name || id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
required={required}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link position-absolute end-0 top-50 translate-middle-y text-secondary p-0 pe-2"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
style={{ zIndex: 10, textDecoration: 'none' }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<i className={`bi ${showPassword ? 'bi-eye' : 'bi-eye-slash'}`}></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface PasswordStrengthMeterProps {
|
||||
password: string;
|
||||
@@ -13,60 +13,71 @@ interface PasswordRequirement {
|
||||
|
||||
const PasswordStrengthMeter: React.FC<PasswordStrengthMeterProps> = ({
|
||||
password,
|
||||
showRequirements = true
|
||||
showRequirements = true,
|
||||
}) => {
|
||||
const requirements: PasswordRequirement[] = [
|
||||
{
|
||||
regex: /.{8,}/,
|
||||
text: "At least 8 characters",
|
||||
met: /.{8,}/.test(password)
|
||||
met: /.{8,}/.test(password),
|
||||
},
|
||||
{
|
||||
regex: /[a-z]/,
|
||||
text: "One lowercase letter",
|
||||
met: /[a-z]/.test(password)
|
||||
met: /[a-z]/.test(password),
|
||||
},
|
||||
{
|
||||
regex: /[A-Z]/,
|
||||
text: "One uppercase letter",
|
||||
met: /[A-Z]/.test(password)
|
||||
text: "One uppercase letter",
|
||||
met: /[A-Z]/.test(password),
|
||||
},
|
||||
{
|
||||
regex: /\d/,
|
||||
text: "One number",
|
||||
met: /\d/.test(password)
|
||||
met: /\d/.test(password),
|
||||
},
|
||||
{
|
||||
regex: /[@$!%*?&]/,
|
||||
text: "One special character (@$!%*?&)",
|
||||
met: /[@$!%*?&]/.test(password)
|
||||
}
|
||||
regex: /[-@$!%*?&#^]/,
|
||||
text: "One special character (-@$!%*?&#^)",
|
||||
met: /[-@$!%*?&#^]/.test(password),
|
||||
},
|
||||
];
|
||||
|
||||
const getPasswordStrength = (): { score: number; label: string; color: string } => {
|
||||
if (!password) return { score: 0, label: '', color: '' };
|
||||
const getPasswordStrength = (): {
|
||||
score: number;
|
||||
label: string;
|
||||
color: string;
|
||||
} => {
|
||||
if (!password) return { score: 0, label: "", color: "" };
|
||||
|
||||
const metRequirements = requirements.filter((req) => req.met).length;
|
||||
const hasCommonPassword = [
|
||||
"password",
|
||||
"123456",
|
||||
"123456789",
|
||||
"qwerty",
|
||||
"abc123",
|
||||
"password123",
|
||||
].includes(password.toLowerCase());
|
||||
|
||||
const metRequirements = requirements.filter(req => req.met).length;
|
||||
const hasCommonPassword = ['password', '123456', '123456789', 'qwerty', 'abc123', 'password123'].includes(password.toLowerCase());
|
||||
|
||||
if (hasCommonPassword) {
|
||||
return { score: 0, label: 'Too Common', color: 'danger' };
|
||||
return { score: 0, label: "Too Common", color: "danger" };
|
||||
}
|
||||
|
||||
switch (metRequirements) {
|
||||
case 0:
|
||||
case 1:
|
||||
return { score: 1, label: 'Very Weak', color: 'danger' };
|
||||
return { score: 1, label: "Very Weak", color: "danger" };
|
||||
case 2:
|
||||
return { score: 2, label: 'Weak', color: 'warning' };
|
||||
return { score: 2, label: "Weak", color: "warning" };
|
||||
case 3:
|
||||
return { score: 3, label: 'Fair', color: 'info' };
|
||||
return { score: 3, label: "Fair", color: "info" };
|
||||
case 4:
|
||||
return { score: 4, label: 'Good', color: 'primary' };
|
||||
return { score: 4, label: "Good", color: "primary" };
|
||||
case 5:
|
||||
return { score: 5, label: 'Strong', color: 'success' };
|
||||
return { score: 5, label: "Strong", color: "success" };
|
||||
default:
|
||||
return { score: 0, label: '', color: '' };
|
||||
return { score: 0, label: "", color: "" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,7 +98,7 @@ const PasswordStrengthMeter: React.FC<PasswordStrengthMeterProps> = ({
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="progress" style={{ height: '4px' }}>
|
||||
<div className="progress" style={{ height: "4px" }}>
|
||||
<div
|
||||
className={`progress-bar bg-${strength.color}`}
|
||||
role="progressbar"
|
||||
@@ -102,17 +113,23 @@ const PasswordStrengthMeter: React.FC<PasswordStrengthMeterProps> = ({
|
||||
{/* Requirements List */}
|
||||
{showRequirements && (
|
||||
<div className="password-requirements">
|
||||
<small className="text-muted d-block mb-1">Password must contain:</small>
|
||||
<ul className="list-unstyled mb-0" style={{ fontSize: '0.75rem' }}>
|
||||
<small className="text-muted d-block mb-1">
|
||||
Password must contain:
|
||||
</small>
|
||||
<ul className="list-unstyled mb-0" style={{ fontSize: "0.75rem" }}>
|
||||
{requirements.map((requirement, index) => (
|
||||
<li key={index} className="d-flex align-items-center mb-1">
|
||||
<i
|
||||
className={`bi ${
|
||||
requirement.met ? 'bi-check-circle-fill text-success' : 'bi-circle text-muted'
|
||||
requirement.met
|
||||
? "bi-check-circle-fill text-success"
|
||||
: "bi-circle text-muted"
|
||||
} me-2`}
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
style={{ fontSize: "0.75rem" }}
|
||||
/>
|
||||
<span className={requirement.met ? 'text-success' : 'text-muted'}>
|
||||
<span
|
||||
className={requirement.met ? "text-success" : "text-muted"}
|
||||
>
|
||||
{requirement.text}
|
||||
</span>
|
||||
</li>
|
||||
@@ -124,4 +141,4 @@ const PasswordStrengthMeter: React.FC<PasswordStrengthMeterProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordStrengthMeter;
|
||||
export default PasswordStrengthMeter;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -54,19 +55,13 @@ const Login: React.FC = () => {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="form-label">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-100"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -125,20 +126,14 @@ const Register: React.FC = () => {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="form-label">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-100"
|
||||
|
||||
145
frontend/src/pages/VerifyEmail.tsx
Normal file
145
frontend/src/pages/VerifyEmail.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { authAPI } from '../services/api';
|
||||
|
||||
const VerifyEmail: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { checkAuth, user } = useAuth();
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [processing, setProcessing] = useState(true);
|
||||
const [resending, setResending] = useState(false);
|
||||
const hasProcessed = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVerification = async () => {
|
||||
// Prevent double execution in React StrictMode
|
||||
if (hasProcessed.current) {
|
||||
return;
|
||||
}
|
||||
hasProcessed.current = true;
|
||||
|
||||
try {
|
||||
const token = searchParams.get('token');
|
||||
|
||||
if (!token) {
|
||||
setError('No verification token provided.');
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the email with the token
|
||||
await authAPI.verifyEmail(token);
|
||||
|
||||
setSuccess(true);
|
||||
setProcessing(false);
|
||||
|
||||
// Refresh user data to update isVerified status
|
||||
await checkAuth();
|
||||
|
||||
// Redirect to home after 3 seconds
|
||||
setTimeout(() => {
|
||||
navigate('/', { replace: true });
|
||||
}, 3000);
|
||||
} catch (err: any) {
|
||||
console.error('Email verification error:', err);
|
||||
const errorData = err.response?.data;
|
||||
|
||||
if (errorData?.code === 'VERIFICATION_TOKEN_EXPIRED') {
|
||||
setError('Your verification link has expired. Please request a new one.');
|
||||
} else if (errorData?.code === 'VERIFICATION_TOKEN_INVALID') {
|
||||
setError('Invalid verification link. The link may have already been used or is incorrect.');
|
||||
} else if (errorData?.code === 'ALREADY_VERIFIED') {
|
||||
setError('Your email is already verified.');
|
||||
} else {
|
||||
setError(errorData?.error || 'Failed to verify email. Please try again.');
|
||||
}
|
||||
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleVerification();
|
||||
}, [searchParams, navigate, checkAuth]);
|
||||
|
||||
const handleResendVerification = async () => {
|
||||
setResending(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await authAPI.resendVerification();
|
||||
setError('');
|
||||
alert('Verification email sent! Please check your inbox.');
|
||||
} catch (err: any) {
|
||||
console.error('Resend verification error:', err);
|
||||
const errorData = err.response?.data;
|
||||
|
||||
if (errorData?.code === 'ALREADY_VERIFIED') {
|
||||
setError('Your email is already verified.');
|
||||
} else if (errorData?.code === 'NO_TOKEN') {
|
||||
setError('You must be logged in to resend the verification email.');
|
||||
} else {
|
||||
setError(errorData?.error || 'Failed to resend verification email. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setResending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row justify-content-center mt-5">
|
||||
<div className="col-md-6">
|
||||
<div className="card">
|
||||
<div className="card-body text-center py-5">
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="spinner-border text-primary mb-3" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>Verifying Your Email...</h5>
|
||||
<p className="text-muted">Please wait while we verify your email address.</p>
|
||||
</>
|
||||
) : success ? (
|
||||
<>
|
||||
<i className="bi bi-check-circle text-success" style={{ fontSize: '3rem' }}></i>
|
||||
<h5 className="mt-3">Email Verified Successfully!</h5>
|
||||
<p className="text-muted">
|
||||
Your email has been verified. You will be redirected to the home page shortly.
|
||||
</p>
|
||||
<Link to="/" className="btn btn-primary mt-3">
|
||||
Go to Home
|
||||
</Link>
|
||||
</>
|
||||
) : error ? (
|
||||
<>
|
||||
<i className="bi bi-exclamation-circle text-danger" style={{ fontSize: '3rem' }}></i>
|
||||
<h5 className="mt-3">Verification Failed</h5>
|
||||
<p className="text-danger">{error}</p>
|
||||
<div className="mt-3">
|
||||
{user && !error.includes('already verified') && (
|
||||
<button
|
||||
className="btn btn-primary me-2"
|
||||
onClick={handleResendVerification}
|
||||
disabled={resending}
|
||||
>
|
||||
{resending ? 'Sending...' : 'Resend Verification Email'}
|
||||
</button>
|
||||
)}
|
||||
<Link to="/" className="btn btn-outline-secondary">
|
||||
Return to Home
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmail;
|
||||
@@ -166,6 +166,8 @@ export const authAPI = {
|
||||
refresh: () => api.post("/auth/refresh"),
|
||||
getCSRFToken: () => api.get("/auth/csrf-token"),
|
||||
getStatus: () => api.get("/auth/status"),
|
||||
verifyEmail: (token: string) => api.post("/auth/verify-email", { token }),
|
||||
resendVerification: () => api.post("/auth/resend-verification"),
|
||||
};
|
||||
|
||||
export const userAPI = {
|
||||
|
||||
Reference in New Issue
Block a user