email verification flow updated
This commit is contained in:
@@ -3,6 +3,7 @@ import { useAuth } from "../contexts/AuthContext";
|
||||
import PasswordStrengthMeter from "./PasswordStrengthMeter";
|
||||
import PasswordInput from "./PasswordInput";
|
||||
import ForgotPasswordModal from "./ForgotPasswordModal";
|
||||
import VerificationCodeModal from "./VerificationCodeModal";
|
||||
|
||||
interface AuthModalProps {
|
||||
show: boolean;
|
||||
@@ -23,6 +24,7 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
||||
const [showVerificationModal, setShowVerificationModal] = useState(false);
|
||||
|
||||
const { login, register } = useAuth();
|
||||
|
||||
@@ -39,6 +41,7 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
setPassword("");
|
||||
setFirstName("");
|
||||
setLastName("");
|
||||
setShowVerificationModal(false);
|
||||
};
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
@@ -68,28 +71,48 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
await login(email, password);
|
||||
onHide();
|
||||
} else {
|
||||
await register({
|
||||
const response = await register({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
username: email.split("@")[0], // Generate username from email
|
||||
});
|
||||
onHide();
|
||||
// Show verification modal after successful registration
|
||||
setShowVerificationModal(true);
|
||||
// Don't call onHide() - keep modal context for verification
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "An error occurred");
|
||||
setError(err.response?.data?.error || "An error occurred");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!show && !showForgotPassword) return null;
|
||||
if (!show && !showForgotPassword && !showVerificationModal) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!showForgotPassword && (
|
||||
{/* Verification Code Modal - shown after signup */}
|
||||
{showVerificationModal && (
|
||||
<VerificationCodeModal
|
||||
show={showVerificationModal}
|
||||
onHide={() => {
|
||||
setShowVerificationModal(false);
|
||||
resetModal();
|
||||
onHide();
|
||||
}}
|
||||
email={email}
|
||||
onVerified={() => {
|
||||
setShowVerificationModal(false);
|
||||
resetModal();
|
||||
onHide();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showForgotPassword && !showVerificationModal && (
|
||||
<div
|
||||
className="modal show d-block"
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -18,6 +18,7 @@ interface CommentThreadProps {
|
||||
isAdmin?: boolean;
|
||||
onAdminDelete?: (commentId: string) => Promise<void>;
|
||||
onAdminRestore?: (commentId: string) => Promise<void>;
|
||||
canReply?: boolean;
|
||||
}
|
||||
|
||||
const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
@@ -33,6 +34,7 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
isAdmin = false,
|
||||
onAdminDelete,
|
||||
onAdminRestore,
|
||||
canReply = true,
|
||||
}) => {
|
||||
const [showReplyForm, setShowReplyForm] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -299,7 +301,7 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
)}
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
{!isEditing && canNest && (
|
||||
{!isEditing && canNest && canReply && (
|
||||
<button
|
||||
className="btn btn-sm btn-link text-decoration-none p-0"
|
||||
onClick={() => setShowReplyForm(!showReplyForm)}
|
||||
@@ -394,6 +396,7 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
isAdmin={isAdmin}
|
||||
onAdminDelete={onAdminDelete}
|
||||
onAdminRestore={onAdminRestore}
|
||||
canReply={canReply}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
249
frontend/src/components/VerificationCodeModal.tsx
Normal file
249
frontend/src/components/VerificationCodeModal.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { authAPI } from "../services/api";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
|
||||
interface VerificationCodeModalProps {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
email: string;
|
||||
onVerified: () => void;
|
||||
}
|
||||
|
||||
const VerificationCodeModal: React.FC<VerificationCodeModalProps> = ({
|
||||
show,
|
||||
onHide,
|
||||
email,
|
||||
onVerified,
|
||||
}) => {
|
||||
const [code, setCode] = useState<string[]>(["", "", "", "", "", ""]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [resending, setResending] = useState(false);
|
||||
const [resendCooldown, setResendCooldown] = useState(0);
|
||||
const [resendSuccess, setResendSuccess] = useState(false);
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const { checkAuth } = useAuth();
|
||||
|
||||
// Handle resend cooldown timer
|
||||
useEffect(() => {
|
||||
if (resendCooldown > 0) {
|
||||
const timer = setTimeout(
|
||||
() => setResendCooldown(resendCooldown - 1),
|
||||
1000
|
||||
);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [resendCooldown]);
|
||||
|
||||
// Focus first input on mount
|
||||
useEffect(() => {
|
||||
if (show && inputRefs.current[0]) {
|
||||
inputRefs.current[0].focus();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
// Clear resend success message after 3 seconds
|
||||
useEffect(() => {
|
||||
if (resendSuccess) {
|
||||
const timer = setTimeout(() => setResendSuccess(false), 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [resendSuccess]);
|
||||
|
||||
const handleInputChange = (index: number, value: string) => {
|
||||
// Only allow digits
|
||||
if (value && !/^\d$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
setError("");
|
||||
|
||||
// Auto-advance to next input
|
||||
if (value && index < 5) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
|
||||
// Auto-submit when all 6 digits entered
|
||||
if (value && index === 5 && newCode.every((d) => d !== "")) {
|
||||
handleVerify(newCode.join(""));
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
|
||||
if (e.key === "Backspace" && !code[index] && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData
|
||||
.getData("text")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (pastedData.length === 6) {
|
||||
const newCode = pastedData.split("");
|
||||
setCode(newCode);
|
||||
handleVerify(pastedData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerify = async (verificationCode: string) => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
await authAPI.verifyEmail(verificationCode);
|
||||
await checkAuth(); // Refresh user data
|
||||
onVerified();
|
||||
onHide();
|
||||
} catch (err: any) {
|
||||
const errorData = err.response?.data;
|
||||
if (errorData?.code === "TOO_MANY_ATTEMPTS") {
|
||||
setError("Too many attempts. Please request a new code.");
|
||||
} else if (errorData?.code === "VERIFICATION_EXPIRED") {
|
||||
setError("Code expired. Please request a new one.");
|
||||
} else if (errorData?.code === "VERIFICATION_INVALID") {
|
||||
setError("Invalid code. Please check and try again.");
|
||||
} else {
|
||||
setError(errorData?.error || "Verification failed. Please try again.");
|
||||
}
|
||||
// Clear code on error
|
||||
setCode(["", "", "", "", "", ""]);
|
||||
inputRefs.current[0]?.focus();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
setResending(true);
|
||||
setError("");
|
||||
setResendSuccess(false);
|
||||
|
||||
try {
|
||||
await authAPI.resendVerification();
|
||||
setResendCooldown(60); // 60 second cooldown
|
||||
setResendSuccess(true);
|
||||
setCode(["", "", "", "", "", ""]);
|
||||
inputRefs.current[0]?.focus();
|
||||
} catch (err: any) {
|
||||
if (err.response?.status === 429) {
|
||||
setError("Please wait before requesting another code.");
|
||||
} else {
|
||||
setError("Failed to resend code. Please try again.");
|
||||
}
|
||||
} finally {
|
||||
setResending(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal show d-block"
|
||||
tabIndex={-1}
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||
>
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header border-0 pb-0">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={onHide}
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div className="modal-body px-4 pb-4 text-center">
|
||||
<i
|
||||
className="bi bi-envelope-check text-success"
|
||||
style={{ fontSize: "3rem" }}
|
||||
></i>
|
||||
<h4 className="mt-3">Verify Your Email</h4>
|
||||
<p className="text-muted">
|
||||
We sent a 6-digit code to <strong>{email}</strong>
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger py-2" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resendSuccess && (
|
||||
<div className="alert alert-success py-2" role="alert">
|
||||
New code sent! Check your email.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 6-digit code input */}
|
||||
<div
|
||||
className="d-flex justify-content-center gap-2 my-4"
|
||||
onPaste={handlePaste}
|
||||
>
|
||||
{code.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
ref={(el) => { inputRefs.current[index] = el; }}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) => handleInputChange(index, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(index, e)}
|
||||
className="form-control text-center"
|
||||
style={{
|
||||
width: "50px",
|
||||
height: "60px",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-success w-100 py-3"
|
||||
onClick={() => handleVerify(code.join(""))}
|
||||
disabled={loading || code.some((d) => d === "")}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
"Verify Email"
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-muted small mb-2">Didn't receive the code?</p>
|
||||
<button
|
||||
className="btn btn-link text-decoration-none p-0"
|
||||
onClick={handleResend}
|
||||
disabled={resending || resendCooldown > 0}
|
||||
>
|
||||
{resendCooldown > 0
|
||||
? `Resend in ${resendCooldown}s`
|
||||
: resending
|
||||
? "Sending..."
|
||||
: "Resend Code"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-muted small mt-3">
|
||||
Check your spam folder if you don't see the email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationCodeModal;
|
||||
@@ -13,11 +13,15 @@ import {
|
||||
resetCSRFToken,
|
||||
} from "../services/api";
|
||||
|
||||
interface RegisterResponse {
|
||||
verificationEmailSent?: boolean;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (data: any) => Promise<void>;
|
||||
register: (data: any) => Promise<RegisterResponse>;
|
||||
googleLogin: (code: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
updateUser: (user: User) => void;
|
||||
@@ -98,11 +102,14 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
await fetchCSRFToken();
|
||||
};
|
||||
|
||||
const register = async (data: any) => {
|
||||
const register = async (data: any): Promise<RegisterResponse> => {
|
||||
const response = await authAPI.register(data);
|
||||
setUser(response.data.user);
|
||||
// Fetch new CSRF token after registration
|
||||
await fetchCSRFToken();
|
||||
return {
|
||||
verificationEmailSent: response.data.verificationEmailSent,
|
||||
};
|
||||
};
|
||||
|
||||
const googleLogin = async (code: string) => {
|
||||
|
||||
@@ -5,18 +5,21 @@ import { forumAPI, addressAPI } from "../services/api";
|
||||
import { uploadFiles, getPublicImageUrl } from "../services/uploadService";
|
||||
import TagInput from "../components/TagInput";
|
||||
import ForumImageUpload from "../components/ForumImageUpload";
|
||||
import VerificationCodeModal from "../components/VerificationCodeModal";
|
||||
import { Address, ForumPost } from "../types";
|
||||
|
||||
const CreateForumPost: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isEditMode = !!id;
|
||||
const { user } = useAuth();
|
||||
const { user, checkAuth } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(isEditMode);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||
const [existingImageKeys, setExistingImageKeys] = useState<string[]>([]);
|
||||
const [showVerificationModal, setShowVerificationModal] = useState(false);
|
||||
const [pendingSubmit, setPendingSubmit] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
@@ -245,11 +248,32 @@ const CreateForumPost: React.FC = () => {
|
||||
navigate(`/forum/${response.data.id}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Check for email verification required error
|
||||
if (
|
||||
err.response?.status === 403 &&
|
||||
err.response?.data?.code === "EMAIL_NOT_VERIFIED"
|
||||
) {
|
||||
setPendingSubmit(true);
|
||||
setShowVerificationModal(true);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
setError(err.response?.data?.error || err.message || `Failed to ${isEditMode ? 'update' : 'create'} post`);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerificationSuccess = async () => {
|
||||
setShowVerificationModal(false);
|
||||
await checkAuth(); // Refresh user data
|
||||
if (pendingSubmit) {
|
||||
setPendingSubmit(false);
|
||||
// Create a synthetic form event to retry submission
|
||||
const syntheticEvent = { preventDefault: () => {} } as React.FormEvent;
|
||||
handleSubmit(syntheticEvent);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
@@ -310,6 +334,23 @@ const CreateForumPost: React.FC = () => {
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{/* Email Verification Warning Banner */}
|
||||
{user && !user.isVerified && (
|
||||
<div className="alert alert-warning d-flex align-items-center mb-4">
|
||||
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div className="flex-grow-1">
|
||||
<strong>Email verification required.</strong> Verify your email to
|
||||
create forum posts.
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-warning btn-sm ms-3"
|
||||
onClick={() => setShowVerificationModal(true)}
|
||||
>
|
||||
Verify Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
{/* Guidelines Card - only show for new posts */}
|
||||
@@ -542,6 +583,19 @@ const CreateForumPost: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Verification Modal */}
|
||||
{user && (
|
||||
<VerificationCodeModal
|
||||
show={showVerificationModal}
|
||||
onHide={() => {
|
||||
setShowVerificationModal(false);
|
||||
setPendingSubmit(false);
|
||||
}}
|
||||
email={user.email || ""}
|
||||
onVerified={handleVerificationSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import LocationForm from "../components/LocationForm";
|
||||
import DeliveryOptions from "../components/DeliveryOptions";
|
||||
import PricingForm from "../components/PricingForm";
|
||||
import RulesForm from "../components/RulesForm";
|
||||
import VerificationCodeModal from "../components/VerificationCodeModal";
|
||||
import { Address } from "../types";
|
||||
import { IMAGE_LIMITS } from "../config/imageLimits";
|
||||
|
||||
@@ -48,9 +49,11 @@ interface ItemFormData {
|
||||
|
||||
const CreateItem: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { user, checkAuth } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [showVerificationModal, setShowVerificationModal] = useState(false);
|
||||
const [pendingSubmit, setPendingSubmit] = useState(false);
|
||||
const [formData, setFormData] = useState<ItemFormData>({
|
||||
name: "",
|
||||
description: "",
|
||||
@@ -265,12 +268,38 @@ const CreateItem: React.FC = () => {
|
||||
|
||||
navigate(`/items/${response.data.id}`);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || err.message || "Failed to create listing");
|
||||
// Check if it's a 403 verification required error
|
||||
if (
|
||||
err.response?.status === 403 &&
|
||||
err.response?.data?.code === "EMAIL_NOT_VERIFIED"
|
||||
) {
|
||||
setPendingSubmit(true);
|
||||
setShowVerificationModal(true);
|
||||
setError("");
|
||||
} else {
|
||||
setError(
|
||||
err.response?.data?.error || err.message || "Failed to create listing"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle successful verification - retry form submission
|
||||
const handleVerificationSuccess = async () => {
|
||||
setShowVerificationModal(false);
|
||||
await checkAuth(); // Refresh user data
|
||||
if (pendingSubmit) {
|
||||
setPendingSubmit(false);
|
||||
// Create a synthetic event to trigger handleSubmit
|
||||
const syntheticEvent = {
|
||||
preventDefault: () => {},
|
||||
} as React.FormEvent<HTMLFormElement>;
|
||||
handleSubmit(syntheticEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
@@ -401,6 +430,26 @@ const CreateItem: React.FC = () => {
|
||||
<div className="col-md-8">
|
||||
<h1>List an Item for Rent</h1>
|
||||
|
||||
{/* Email verification warning banner */}
|
||||
{user && !user.isVerified && (
|
||||
<div
|
||||
className="alert alert-warning d-flex align-items-center"
|
||||
role="alert"
|
||||
>
|
||||
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div className="flex-grow-1">
|
||||
Verify your email to create a listing.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-warning btn-sm"
|
||||
onClick={() => setShowVerificationModal(true)}
|
||||
>
|
||||
Verify Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
@@ -484,10 +533,7 @@ const CreateItem: React.FC = () => {
|
||||
onTierToggle={handleTierToggle}
|
||||
/>
|
||||
|
||||
<RulesForm
|
||||
rules={formData.rules || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<RulesForm rules={formData.rules || ""} onChange={handleChange} />
|
||||
|
||||
<div className="d-grid gap-2 mb-5">
|
||||
<button
|
||||
@@ -508,6 +554,19 @@ const CreateItem: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification Code Modal */}
|
||||
{showVerificationModal && user && (
|
||||
<VerificationCodeModal
|
||||
show={showVerificationModal}
|
||||
onHide={() => {
|
||||
setShowVerificationModal(false);
|
||||
setPendingSubmit(false);
|
||||
}}
|
||||
email={user.email || ""}
|
||||
onVerified={handleVerificationSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,10 +9,11 @@ import PostStatusBadge from '../components/PostStatusBadge';
|
||||
import CommentThread from '../components/CommentThread';
|
||||
import CommentForm from '../components/CommentForm';
|
||||
import AuthButton from '../components/AuthButton';
|
||||
import VerificationCodeModal from '../components/VerificationCodeModal';
|
||||
|
||||
const ForumPostDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { user } = useAuth();
|
||||
const { user, checkAuth } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [post, setPost] = useState<ForumPost | null>(null);
|
||||
@@ -20,6 +21,7 @@ const ForumPostDetail: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
const [showAdminModal, setShowAdminModal] = useState(false);
|
||||
const [showVerificationModal, setShowVerificationModal] = useState(false);
|
||||
const [adminAction, setAdminAction] = useState<{
|
||||
type: 'deletePost' | 'deleteComment' | 'restorePost' | 'restoreComment' | 'closePost' | 'reopenPost';
|
||||
id?: string;
|
||||
@@ -68,6 +70,14 @@ const ForumPostDetail: React.FC = () => {
|
||||
});
|
||||
await fetchPost(); // Refresh to get new comment
|
||||
} catch (err: any) {
|
||||
// Check for email verification required error
|
||||
if (
|
||||
err.response?.status === 403 &&
|
||||
err.response?.data?.code === "EMAIL_NOT_VERIFIED"
|
||||
) {
|
||||
setShowVerificationModal(true);
|
||||
throw new Error('Email verification required to comment.');
|
||||
}
|
||||
throw new Error(err.response?.data?.error || err.message || 'Failed to post comment');
|
||||
}
|
||||
};
|
||||
@@ -93,10 +103,23 @@ const ForumPostDetail: React.FC = () => {
|
||||
});
|
||||
await fetchPost(); // Refresh to get new reply
|
||||
} catch (err: any) {
|
||||
// Check for email verification required error
|
||||
if (
|
||||
err.response?.status === 403 &&
|
||||
err.response?.data?.code === "EMAIL_NOT_VERIFIED"
|
||||
) {
|
||||
setShowVerificationModal(true);
|
||||
throw new Error('Email verification required to reply.');
|
||||
}
|
||||
throw new Error(err.response?.data?.error || err.message || 'Failed to post reply');
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerificationSuccess = async () => {
|
||||
setShowVerificationModal(false);
|
||||
await checkAuth(); // Refresh user data
|
||||
};
|
||||
|
||||
const handleEditComment = async (
|
||||
commentId: string,
|
||||
content: string,
|
||||
@@ -490,6 +513,7 @@ const ForumPostDetail: React.FC = () => {
|
||||
isAdmin={isAdmin}
|
||||
onAdminDelete={handleAdminDeleteComment}
|
||||
onAdminRestore={handleAdminRestoreComment}
|
||||
canReply={!!user}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -529,6 +553,21 @@ const ForumPostDetail: React.FC = () => {
|
||||
|
||||
{post.status !== 'closed' && user ? (
|
||||
<div>
|
||||
{/* Email Verification Warning for unverified users */}
|
||||
{!user.isVerified && (
|
||||
<div className="alert alert-warning d-flex align-items-center mb-3">
|
||||
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div className="flex-grow-1">
|
||||
<strong>Email verification required.</strong> Verify your email to comment.
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-warning btn-sm ms-3"
|
||||
onClick={() => setShowVerificationModal(true)}
|
||||
>
|
||||
Verify Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<h6>Add a comment</h6>
|
||||
<CommentForm
|
||||
onSubmit={handleAddComment}
|
||||
@@ -673,6 +712,16 @@ const ForumPostDetail: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Verification Modal */}
|
||||
{user && (
|
||||
<VerificationCodeModal
|
||||
show={showVerificationModal}
|
||||
onHide={() => setShowVerificationModal(false)}
|
||||
email={user.email || ""}
|
||||
onVerified={handleVerificationSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,15 +5,18 @@ import { useAuth } from "../contexts/AuthContext";
|
||||
import { itemAPI, rentalAPI } from "../services/api";
|
||||
import { getPublicImageUrl } from "../services/uploadService";
|
||||
import EmbeddedStripeCheckout from "../components/EmbeddedStripeCheckout";
|
||||
import VerificationCodeModal from "../components/VerificationCodeModal";
|
||||
|
||||
const RentItem: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { user, checkAuth } = useAuth();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [item, setItem] = useState<Item | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showVerificationModal, setShowVerificationModal] = useState(false);
|
||||
const [pendingSubmit, setPendingSubmit] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
deliveryMethod: "pickup" as "pickup" | "delivery",
|
||||
@@ -163,12 +166,31 @@ const RentItem: React.FC = () => {
|
||||
await rentalAPI.createRental(rentalData);
|
||||
setCompleted(true);
|
||||
} catch (error: any) {
|
||||
// Check for email verification required error
|
||||
if (
|
||||
error.response?.status === 403 &&
|
||||
error.response?.data?.code === "EMAIL_NOT_VERIFIED"
|
||||
) {
|
||||
setPendingSubmit(true);
|
||||
setShowVerificationModal(true);
|
||||
return;
|
||||
}
|
||||
setError(
|
||||
error.response?.data?.error || "Failed to create rental request"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerificationSuccess = async () => {
|
||||
setShowVerificationModal(false);
|
||||
await checkAuth(); // Refresh user data
|
||||
if (pendingSubmit) {
|
||||
setPendingSubmit(false);
|
||||
// Retry the rental submission
|
||||
handleFreeBorrow();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
@@ -213,6 +235,23 @@ const RentItem: React.FC = () => {
|
||||
<div className="col-md-8">
|
||||
<h1>Renting {item.name}</h1>
|
||||
|
||||
{/* Email Verification Warning Banner */}
|
||||
{user && !user.isVerified && (
|
||||
<div className="alert alert-warning d-flex align-items-center mb-4">
|
||||
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div className="flex-grow-1">
|
||||
<strong>Email verification required.</strong> Verify your email
|
||||
to book rentals.
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-warning btn-sm ms-3"
|
||||
onClick={() => setShowVerificationModal(true)}
|
||||
>
|
||||
Verify Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
@@ -426,6 +465,19 @@ const RentItem: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Verification Modal */}
|
||||
{user && (
|
||||
<VerificationCodeModal
|
||||
show={showVerificationModal}
|
||||
onHide={() => {
|
||||
setShowVerificationModal(false);
|
||||
setPendingSubmit(false);
|
||||
}}
|
||||
email={user.email || ""}
|
||||
onVerified={handleVerificationSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,93 +1,234 @@
|
||||
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';
|
||||
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 { checkAuth, user, loading: authLoading } = useAuth();
|
||||
const [error, setError] = useState<string>("");
|
||||
const [errorCode, setErrorCode] = useState<string>("");
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [processing, setProcessing] = useState(true);
|
||||
const [resending, setResending] = useState(false);
|
||||
const [resendSuccess, setResendSuccess] = useState(false);
|
||||
const [resendCooldown, setResendCooldown] = useState(0);
|
||||
const [showManualEntry, setShowManualEntry] = useState(false);
|
||||
const [code, setCode] = useState<string[]>(["", "", "", "", "", ""]);
|
||||
const [verifying, setVerifying] = useState(false);
|
||||
const hasProcessed = useRef(false);
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
// Handle resend cooldown timer
|
||||
useEffect(() => {
|
||||
if (resendCooldown > 0) {
|
||||
const timer = setTimeout(
|
||||
() => setResendCooldown(resendCooldown - 1),
|
||||
1000
|
||||
);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [resendCooldown]);
|
||||
|
||||
// Clear resend success after 3 seconds
|
||||
useEffect(() => {
|
||||
if (resendSuccess) {
|
||||
const timer = setTimeout(() => setResendSuccess(false), 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [resendSuccess]);
|
||||
|
||||
// Check authentication and handle verification
|
||||
useEffect(() => {
|
||||
// Wait for auth to finish loading
|
||||
if (authLoading) return;
|
||||
|
||||
const handleVerification = async () => {
|
||||
// Prevent double execution in React StrictMode
|
||||
if (hasProcessed.current) {
|
||||
return;
|
||||
}
|
||||
if (hasProcessed.current) return;
|
||||
hasProcessed.current = true;
|
||||
|
||||
try {
|
||||
const token = searchParams.get('token');
|
||||
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);
|
||||
// If not logged in, redirect to login with return URL
|
||||
if (!user) {
|
||||
const returnUrl = token
|
||||
? `/verify-email?token=${token}`
|
||||
: "/verify-email";
|
||||
navigate(`/?login=true&redirect=${encodeURIComponent(returnUrl)}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If user is already verified, show success
|
||||
if (user.isVerified) {
|
||||
setSuccess(true);
|
||||
setProcessing(false);
|
||||
setTimeout(() => navigate("/", { replace: true }), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh user data to update isVerified status
|
||||
// If no token in URL, show manual entry form
|
||||
if (!token) {
|
||||
setShowManualEntry(true);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-verify with token from URL
|
||||
try {
|
||||
await authAPI.verifyEmail(token);
|
||||
setSuccess(true);
|
||||
setProcessing(false);
|
||||
await checkAuth();
|
||||
|
||||
// Redirect to home after 3 seconds
|
||||
setTimeout(() => {
|
||||
navigate('/', { replace: true });
|
||||
}, 3000);
|
||||
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.');
|
||||
}
|
||||
|
||||
handleVerificationError(err);
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleVerification();
|
||||
}, [searchParams, navigate, checkAuth]);
|
||||
}, [searchParams, navigate, checkAuth, user, authLoading]);
|
||||
|
||||
const handleVerificationError = (err: any) => {
|
||||
const errorData = err.response?.data;
|
||||
const code = errorData?.code || "";
|
||||
setErrorCode(code);
|
||||
|
||||
switch (code) {
|
||||
case "VERIFICATION_EXPIRED":
|
||||
setError("Your verification code has expired. Please request a new one.");
|
||||
setShowManualEntry(true);
|
||||
break;
|
||||
case "VERIFICATION_INVALID":
|
||||
setError("Invalid verification code. Please check and try again.");
|
||||
setShowManualEntry(true);
|
||||
break;
|
||||
case "TOO_MANY_ATTEMPTS":
|
||||
setError("Too many attempts. Please request a new code.");
|
||||
setShowManualEntry(true);
|
||||
break;
|
||||
case "ALREADY_VERIFIED":
|
||||
setError("Your email is already verified.");
|
||||
break;
|
||||
case "NO_CODE":
|
||||
setError("No verification code found. Please request a new one.");
|
||||
setShowManualEntry(true);
|
||||
break;
|
||||
default:
|
||||
setError(errorData?.error || "Failed to verify email. Please try again.");
|
||||
setShowManualEntry(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (index: number, value: string) => {
|
||||
if (value && !/^\d$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
setError("");
|
||||
setErrorCode("");
|
||||
|
||||
if (value && index < 5) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
|
||||
if (value && index === 5 && newCode.every((d) => d !== "")) {
|
||||
handleManualVerify(newCode.join(""));
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
|
||||
if (e.key === "Backspace" && !code[index] && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData
|
||||
.getData("text")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (pastedData.length === 6) {
|
||||
const newCode = pastedData.split("");
|
||||
setCode(newCode);
|
||||
handleManualVerify(pastedData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualVerify = async (verificationCode: string) => {
|
||||
setVerifying(true);
|
||||
setError("");
|
||||
setErrorCode("");
|
||||
|
||||
try {
|
||||
await authAPI.verifyEmail(verificationCode);
|
||||
setSuccess(true);
|
||||
await checkAuth();
|
||||
setTimeout(() => navigate("/", { replace: true }), 3000);
|
||||
} catch (err: any) {
|
||||
handleVerificationError(err);
|
||||
setCode(["", "", "", "", "", ""]);
|
||||
inputRefs.current[0]?.focus();
|
||||
} finally {
|
||||
setVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendVerification = async () => {
|
||||
setResending(true);
|
||||
setError('');
|
||||
setError("");
|
||||
setErrorCode("");
|
||||
setResendSuccess(false);
|
||||
|
||||
try {
|
||||
await authAPI.resendVerification();
|
||||
setError('');
|
||||
alert('Verification email sent! Please check your inbox.');
|
||||
setResendSuccess(true);
|
||||
setResendCooldown(60);
|
||||
setCode(["", "", "", "", "", ""]);
|
||||
inputRefs.current[0]?.focus();
|
||||
} 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.');
|
||||
if (errorData?.code === "ALREADY_VERIFIED") {
|
||||
setError("Your email is already verified.");
|
||||
} else if (err.response?.status === 429) {
|
||||
setError("Please wait before requesting another code.");
|
||||
} else {
|
||||
setError(errorData?.error || 'Failed to resend verification email. Please try again.');
|
||||
setError(
|
||||
errorData?.error ||
|
||||
"Failed to resend verification email. Please try again."
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setResending(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading while auth is initializing
|
||||
if (authLoading) {
|
||||
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">
|
||||
<div className="spinner-border text-primary mb-3" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>Loading...</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row justify-content-center mt-5">
|
||||
@@ -96,38 +237,136 @@ const VerifyEmail: React.FC = () => {
|
||||
<div className="card-body text-center py-5">
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="spinner-border text-primary mb-3" role="status">
|
||||
<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>
|
||||
<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>
|
||||
<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.
|
||||
Your email has been verified. You will be redirected
|
||||
shortly.
|
||||
</p>
|
||||
<Link to="/" className="btn btn-primary mt-3">
|
||||
<Link to="/" className="btn btn-success mt-3">
|
||||
Go to Home
|
||||
</Link>
|
||||
</>
|
||||
) : showManualEntry ? (
|
||||
<>
|
||||
<i
|
||||
className="bi bi-envelope-check text-success"
|
||||
style={{ fontSize: "3rem" }}
|
||||
></i>
|
||||
<h5 className="mt-3">Enter Verification Code</h5>
|
||||
<p className="text-muted">
|
||||
Enter the 6-digit code sent to your email
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger py-2" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resendSuccess && (
|
||||
<div className="alert alert-success py-2" role="alert">
|
||||
New code sent! Check your email.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 6-digit code input */}
|
||||
<div
|
||||
className="d-flex justify-content-center gap-2 my-4"
|
||||
onPaste={handlePaste}
|
||||
>
|
||||
{code.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
ref={(el) => { inputRefs.current[index] = el; }}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) =>
|
||||
handleInputChange(index, e.target.value)
|
||||
}
|
||||
onKeyDown={(e) => handleKeyDown(index, e)}
|
||||
className="form-control text-center"
|
||||
style={{
|
||||
width: "50px",
|
||||
height: "60px",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
disabled={verifying}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-success w-100 py-3"
|
||||
onClick={() => handleManualVerify(code.join(""))}
|
||||
disabled={verifying || code.some((d) => d === "")}
|
||||
>
|
||||
{verifying ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
"Verify Email"
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-muted small mb-2">
|
||||
Didn't receive the code?
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-link text-decoration-none p-0"
|
||||
onClick={handleResendVerification}
|
||||
disabled={resending || resendCooldown > 0}
|
||||
>
|
||||
{resendCooldown > 0
|
||||
? `Resend in ${resendCooldown}s`
|
||||
: resending
|
||||
? "Sending..."
|
||||
: "Resend Code"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-muted small mt-3">
|
||||
Check your spam folder if you don't see the email.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className="btn btn-outline-secondary mt-3"
|
||||
>
|
||||
Return to Home
|
||||
</Link>
|
||||
</>
|
||||
) : error ? (
|
||||
<>
|
||||
<i className="bi bi-exclamation-circle text-danger" style={{ fontSize: '3rem' }}></i>
|
||||
<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>
|
||||
|
||||
@@ -162,7 +162,7 @@ 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 }),
|
||||
verifyEmail: (code: string) => api.post("/auth/verify-email", { code }),
|
||||
resendVerification: () => api.post("/auth/resend-verification"),
|
||||
forgotPassword: (email: string) =>
|
||||
api.post("/auth/forgot-password", { email }),
|
||||
|
||||
Reference in New Issue
Block a user