Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/passport-client/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ const define = {
process.env.PRIORITY_FEED_PROVIDER_URLS
)
}
: {}),
...(process.env.TURNSTILE_SITE_KEY !== undefined
? {
"process.env.TURNSTILE_SITE_KEY": JSON.stringify(
process.env.TURNSTILE_SITE_KEY
)
}
: {})
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Turnstile } from "@marsidev/react-turnstile";
import { PCDCrypto } from "@pcd/passport-crypto";
import {
ConfirmEmailResult,
Expand Down Expand Up @@ -42,6 +43,9 @@ export const NewAlreadyRegisteredScreen: React.FC = () => {
useState(false);
const [password, setPassword] = useState("");
const [verifyingCode, setVerifyingCode] = useState(false);
const [showCaptcha, setShowCaptcha] = useState(false);
const [captchaToken, setCaptchaToken] = useState<string | undefined>();
const requiresCaptcha = !!appConfig.turnstileSiteKey;

const verifyToken = useCallback(
async (token: string) => {
Expand Down Expand Up @@ -90,6 +94,26 @@ export const NewAlreadyRegisteredScreen: React.FC = () => {
[email, identityCommitment, verifyToken]
);

const sendPasswordResetEmail = useCallback(
async (token?: string) => {
if (!email || !identityCommitment) {
return;
}
setSendingConfirmationEmail(true);
const emailConfirmationResult = await requestConfirmationEmail(
appConfig.zupassServer,
email,
true,
token
);
handleConfirmationEmailResult(emailConfirmationResult);
// Reset captcha token after use
setCaptchaToken(undefined);
setShowCaptcha(false);
},
[email, identityCommitment, handleConfirmationEmailResult]
);

const onOverwriteClick = useCallback(async () => {
if (!email || !identityCommitment) {
return;
Expand All @@ -99,14 +123,27 @@ export const NewAlreadyRegisteredScreen: React.FC = () => {
identityCommitment
});

setSendingConfirmationEmail(true);
const emailConfirmationResult = await requestConfirmationEmail(
appConfig.zupassServer,
email,
true
);
handleConfirmationEmailResult(emailConfirmationResult);
}, [email, identityCommitment, handleConfirmationEmailResult]);
// If captcha is required, show it first
if (requiresCaptcha && !captchaToken) {
setShowCaptcha(true);
return;
}

await sendPasswordResetEmail(captchaToken);
}, [
email,
identityCommitment,
requiresCaptcha,
captchaToken,
sendPasswordResetEmail
]);

// Auto-send email once captcha is verified
useEffect(() => {
if (showCaptcha && captchaToken) {
sendPasswordResetEmail(captchaToken);
}
}, [captchaToken, showCaptcha, sendPasswordResetEmail]);

const onLoginWithMasterPasswordClick = useCallback(() => {
requestLogToServer(
Expand Down Expand Up @@ -262,16 +299,53 @@ export const NewAlreadyRegisteredScreen: React.FC = () => {
<Button2 onClick={onCancelClick} variant="secondary">
Cancel
</Button2>
<div onClick={onOverwriteClick} style={{ cursor: "pointer" }}>
<Typography
color={"#1E2C50"}
fontSize={14}
fontWeight={500}
family="Rubik"
>
{salt ? "Forgot Password?" : "Lost Sync Key?"}
</Typography>
</div>
{showCaptcha && requiresCaptcha ? (
<>
<Typography
fontSize={16}
fontWeight={400}
color="#1E2C50"
family="Rubik"
style={{ textAlign: "center", marginTop: "16px" }}
>
Please complete the captcha to reset your password
</Typography>
<Turnstile
siteKey={appConfig.turnstileSiteKey!}
onSuccess={(token) => {
setCaptchaToken(token);
}}
onError={() => {
setError("Captcha verification failed. Please try again.");
setCaptchaToken(undefined);
setShowCaptcha(false);
}}
onExpire={() => {
setCaptchaToken(undefined);
}}
/>
<Button2
variant="secondary"
onClick={() => {
setShowCaptcha(false);
setCaptchaToken(undefined);
}}
>
Cancel
</Button2>
</>
) : (
<div onClick={onOverwriteClick} style={{ cursor: "pointer" }}>
<Typography
color={"#1E2C50"}
fontSize={14}
fontWeight={500}
family="Rubik"
>
{salt ? "Forgot Password?" : "Lost Sync Key?"}
</Typography>
</div>
)}
</InputsContainer>
</LoginContainer>
</AppContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Turnstile } from "@marsidev/react-turnstile";
import {
ConfirmEmailResult,
getNamedAPIErrorMessage,
Expand Down Expand Up @@ -56,7 +57,13 @@ const SendEmailVerification = ({
const [verifyingCode, setVerifyingCode] = useState(false);
const [loadingAccount, setLoadingAccount] = useState(false);
const [token, setToken] = useState("");
const loadingPage = loadingAccount || emailSending || !emailSent;
const [captchaToken, setCaptchaToken] = useState<string | undefined>();
const requiresCaptcha = !!appConfig.turnstileSiteKey;
// Only show loading page if we're not waiting for captcha
const waitingForCaptcha =
requiresCaptcha && !captchaToken && !triedSendingEmail;
const loadingPage =
(loadingAccount || emailSending || !emailSent) && !waitingForCaptcha;

const verifyToken = useCallback(
async (token: string) => {
Expand Down Expand Up @@ -167,23 +174,41 @@ const SendEmailVerification = ({
[dispatch, email, identity.commitment, verifyToken]
);

const doRequestConfirmationEmail = useCallback(async () => {
setEmailSending(true);
const confirmationEmailResult = await requestConfirmationEmail(
appConfig.zupassServer,
email,
false
);
setEmailSending(false);
const doRequestConfirmationEmail = useCallback(
async (captchaToken?: string) => {
// Double-check: never send email if captcha is required but token is missing
if (requiresCaptcha && !captchaToken) {
return;
}

setEmailSending(true);
const confirmationEmailResult = await requestConfirmationEmail(
appConfig.zupassServer,
email,
false,
captchaToken
);
setEmailSending(false);

handleConfirmationEmailResult(confirmationEmailResult);
}, [email, handleConfirmationEmailResult]);
handleConfirmationEmailResult(confirmationEmailResult);
// Reset captcha token after use
setCaptchaToken(undefined);
},
[email, handleConfirmationEmailResult, requiresCaptcha]
);

useEffect(() => {
if (triedSendingEmail) return;
// Don't auto-send if captcha is required and not yet verified
if (requiresCaptcha && !captchaToken) return;
setTriedSendingEmail(true);
doRequestConfirmationEmail();
}, [triedSendingEmail, doRequestConfirmationEmail]);
doRequestConfirmationEmail(captchaToken);
}, [
triedSendingEmail,
doRequestConfirmationEmail,
requiresCaptcha,
captchaToken
]);

// Verify the code the user entered.
const onSubmit = useCallback(
Expand All @@ -194,6 +219,60 @@ const SendEmailVerification = ({
[verifyToken, token]
);

// Show captcha if required and we haven't sent email yet
if (waitingForCaptcha) {
return (
<AppContainer bg="gray" fullscreen>
<LoginContainer>
<LoginTitleContainer>
<Typography fontSize={24} fontWeight={800} color="#1E2C50">
COMPLETE VERIFICATION
</Typography>
<Typography
fontSize={16}
fontWeight={400}
color="#1E2C50"
family="Rubik"
>
Please complete the verification below to continue.
</Typography>
</LoginTitleContainer>
<LoginForm>
<Input2 variant="primary" value={email} disabled />
{appConfig.turnstileSiteKey && (
<Turnstile
siteKey={appConfig.turnstileSiteKey}
onSuccess={(token) => {
setCaptchaToken(token);
}}
onError={() => {
setError("Captcha verification failed. Please try again.");
setCaptchaToken(undefined);
}}
onExpire={() => {
setCaptchaToken(undefined);
}}
/>
)}
{error && (
<Typography
fontSize={14}
fontWeight={400}
color="#FF0000"
family="Rubik"
>
{error}
</Typography>
)}
<Button2 variant="secondary" onClick={() => navigate("/")}>
Cancel
</Button2>
</LoginForm>
</LoginContainer>
</AppContainer>
);
}

if (loadingPage) {
let loaderText = "";
if (verifyingCode) {
Expand Down
Loading
Loading