Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants. Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useRef, useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { Button, Container } from "@/components/ui";
|
||
import { getStoredToken, loginWithRedirect } from "@/lib/auth";
|
||
import { identityApi } from "@/lib/identity";
|
||
import {
|
||
isValidMobile,
|
||
normalizeOtpCode,
|
||
normalizePhone,
|
||
persianToEnglish,
|
||
} from "@/lib/phone";
|
||
|
||
export default function VerifyMobilePage() {
|
||
const router = useRouter();
|
||
const [mobile, setMobile] = useState("");
|
||
const [code, setCode] = useState("");
|
||
const [countdown, setCountdown] = useState(0);
|
||
const [step, setStep] = useState<"mobile" | "code">("mobile");
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState("");
|
||
const verifyingRef = useRef(false);
|
||
|
||
useEffect(() => {
|
||
if (!getStoredToken()) {
|
||
void loginWithRedirect();
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (countdown <= 0) return;
|
||
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
|
||
return () => clearTimeout(timer);
|
||
}, [countdown]);
|
||
|
||
const handleRequestOtp = async () => {
|
||
const token = getStoredToken();
|
||
if (!token) return;
|
||
setError("");
|
||
const normalized = normalizePhone(mobile);
|
||
if (!isValidMobile(normalized)) {
|
||
setError("شماره موبایل صحیح نیست");
|
||
return;
|
||
}
|
||
setMobile(normalized);
|
||
setLoading(true);
|
||
try {
|
||
const res = await identityApi.requestMobileVerify(normalized, token);
|
||
setCountdown(res.expires_in);
|
||
setStep("code");
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : "ارسال کد ناموفق بود");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleVerify = useCallback(async () => {
|
||
const token = getStoredToken();
|
||
if (!token || verifyingRef.current) return;
|
||
const normalizedCode = normalizeOtpCode(code);
|
||
if (normalizedCode.length !== 4) {
|
||
setError("کد تأیید باید ۴ رقم باشد");
|
||
return;
|
||
}
|
||
verifyingRef.current = true;
|
||
setLoading(true);
|
||
let keepLoading = false;
|
||
try {
|
||
await identityApi.verifyMobile(mobile, normalizedCode, token);
|
||
keepLoading = true;
|
||
router.replace("/dashboard");
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : "تأیید ناموفق بود");
|
||
} finally {
|
||
if (!keepLoading) {
|
||
setLoading(false);
|
||
verifyingRef.current = false;
|
||
}
|
||
}
|
||
}, [code, mobile, router]);
|
||
|
||
return (
|
||
<Container className="py-16">
|
||
<section className="mx-auto max-w-md rounded-2xl border border-gray-100 bg-white p-8 shadow-sm">
|
||
<h1 className="text-2xl font-bold text-secondary">تأیید شماره موبایل</h1>
|
||
<p className="mt-2 text-sm text-gray-600">
|
||
برای استفاده از سامانه یکپارچه، تأیید شماره موبایل الزامی است.
|
||
</p>
|
||
|
||
{error && (
|
||
<p className="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
|
||
)}
|
||
|
||
{step === "mobile" ? (
|
||
<div className="mt-6 space-y-4">
|
||
<input
|
||
type="tel"
|
||
dir="ltr"
|
||
placeholder="09123456789"
|
||
value={mobile}
|
||
onChange={(e) => setMobile(persianToEnglish(e.target.value))}
|
||
className="w-full rounded-xl border border-gray-200 px-4 py-2.5"
|
||
/>
|
||
<Button className="w-full" onClick={handleRequestOtp} loading={loading} loadingText="در حال ارسال...">
|
||
دریافت کد تأیید
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<div className="mt-6 space-y-4">
|
||
<input
|
||
type="text"
|
||
dir="ltr"
|
||
maxLength={4}
|
||
value={code}
|
||
onChange={(e) => setCode(normalizeOtpCode(e.target.value))}
|
||
className="w-full rounded-xl border border-gray-200 px-4 py-2.5 text-center font-mono tracking-widest"
|
||
/>
|
||
<Button
|
||
className="w-full"
|
||
onClick={handleVerify}
|
||
loading={loading}
|
||
loadingText="در حال تأیید..."
|
||
disabled={code.length !== 4}
|
||
>
|
||
تأیید موبایل
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</section>
|
||
</Container>
|
||
);
|
||
}
|