feat: support inline-captcha in login page (#3970)

This commit is contained in:
DacongDA
2025-07-19 01:12:07 +08:00
committed by GitHub
parent 7aa0b2e63f
commit ea68e6c2dc
3 changed files with 104 additions and 40 deletions

View File

@@ -437,18 +437,26 @@ class LoginPage extends React.Component {
values["password"] = passwordCipher; values["password"] = passwordCipher;
} }
const captchaRule = this.getCaptchaRule(this.getApplicationObj()); const captchaRule = this.getCaptchaRule(this.getApplicationObj());
if (captchaRule === CaptchaRule.Always) { const application = this.getApplicationObj();
this.setState({ const noModal = application?.signinItems.map(signinItem => signinItem.name === "Captcha" && signinItem.rule === "inline").includes(true);
openCaptchaModal: true, if (!noModal) {
values: values, if (captchaRule === CaptchaRule.Always) {
}); this.setState({
return; openCaptchaModal: true,
} else if (captchaRule === CaptchaRule.Dynamic) { values: values,
this.checkCaptchaStatus(values); });
return; return;
} else if (captchaRule === CaptchaRule.InternetOnly) { } else if (captchaRule === CaptchaRule.Dynamic) {
this.checkCaptchaStatus(values); this.checkCaptchaStatus(values);
return; return;
} else if (captchaRule === CaptchaRule.InternetOnly) {
this.checkCaptchaStatus(values);
return;
}
} else {
values["captchaType"] = this.state?.captchaValues?.captchaType;
values["captchaToken"] = this.state?.captchaValues?.captchaToken;
values["clientSecret"] = this.state?.captchaValues?.clientSecret;
} }
} }
this.login(values); this.login(values);
@@ -775,7 +783,7 @@ class LoginPage extends React.Component {
</> </>
} }
{ {
this.renderCaptchaModal(application) application?.signinItems.map(signinItem => signinItem.name === "Captcha" && signinItem.rule === "inline").includes(true) ? null : this.renderCaptchaModal(application, false)
} }
</Form.Item> </Form.Item>
); );
@@ -819,6 +827,8 @@ class LoginPage extends React.Component {
</Form.Item> </Form.Item>
</div> </div>
); );
} else if (signinItem.name === "Captcha" && signinItem.rule === "inline") {
return this.renderCaptchaModal(application, true);
} else if (signinItem.name.startsWith("Text ") || signinItem?.isCustom) { } else if (signinItem.name.startsWith("Text ") || signinItem?.isCustom) {
return ( return (
<div key={resultItemKey} dangerouslySetInnerHTML={{__html: signinItem.customCss}} /> <div key={resultItemKey} dangerouslySetInnerHTML={{__html: signinItem.customCss}} />
@@ -964,7 +974,7 @@ class LoginPage extends React.Component {
}); });
} }
renderCaptchaModal(application) { renderCaptchaModal(application, noModal) {
if (this.getCaptchaRule(this.getApplicationObj()) === CaptchaRule.Never) { if (this.getCaptchaRule(this.getApplicationObj()) === CaptchaRule.Never) {
return null; return null;
} }
@@ -993,6 +1003,12 @@ class LoginPage extends React.Component {
owner={provider.owner} owner={provider.owner}
name={provider.name} name={provider.name}
visible={this.state.openCaptchaModal} visible={this.state.openCaptchaModal}
noModal={noModal}
onUpdateToken={(captchaType, captchaToken, clientSecret) => {
this.setState({captchaValues: {
captchaType, captchaToken, clientSecret,
}});
}}
onOk={(captchaType, captchaToken, clientSecret) => { onOk={(captchaType, captchaToken, clientSecret) => {
const values = this.state.values; const values = this.state.values;
values["captchaType"] = captchaType; values["captchaType"] = captchaType;

View File

@@ -20,7 +20,7 @@ import {CaptchaWidget} from "../CaptchaWidget";
import {SafetyOutlined} from "@ant-design/icons"; import {SafetyOutlined} from "@ant-design/icons";
export const CaptchaModal = (props) => { export const CaptchaModal = (props) => {
const {owner, name, visible, onOk, onCancel, isCurrentProvider} = props; const {owner, name, visible, onOk, onUpdateToken, onCancel, isCurrentProvider, noModal} = props;
const [captchaType, setCaptchaType] = React.useState("none"); const [captchaType, setCaptchaType] = React.useState("none");
const [clientId, setClientId] = React.useState(""); const [clientId, setClientId] = React.useState("");
@@ -36,16 +36,16 @@ export const CaptchaModal = (props) => {
const defaultInputRef = React.useRef(null); const defaultInputRef = React.useRef(null);
useEffect(() => { useEffect(() => {
if (visible) { if (visible || noModal) {
loadCaptcha(); loadCaptcha();
} else { } else {
handleCancel(); handleCancel();
setOpen(false); setOpen(false);
} }
}, [visible]); }, [visible, noModal]);
useEffect(() => { useEffect(() => {
if (captchaToken !== "" && captchaType !== "Default") { if (captchaToken !== "" && captchaType !== "Default" && !noModal) {
handleOk(); handleOk();
} }
}, [captchaToken]); }, [captchaToken]);
@@ -81,6 +81,36 @@ export const CaptchaModal = (props) => {
}; };
const renderDefaultCaptcha = () => { const renderDefaultCaptcha = () => {
if (noModal) {
return (
<Row style={{textAlign: "center"}}>
<Col
style={{flex: noModal ? "70%" : "100%"}}>
<Input
ref={defaultInputRef}
value={captchaToken}
prefix={<SafetyOutlined />}
placeholder={i18next.t("general:Captcha")}
onChange={(e) => onChange(e.target.value)}
/>
</Col>
<Col
style={{
flex: noModal ? "30%" : "100%",
}}
>
<img src={`data:image/png;base64,${captchaImg}`}
onClick={loadCaptcha}
style={{
borderRadius: "5px",
border: "1px solid #ccc",
marginBottom: "20px",
width: "100%",
}} alt="captcha" />
</Col>
</Row>
);
}
return ( return (
<Col style={{textAlign: "center"}}> <Col style={{textAlign: "center"}}>
<div style={{display: "inline-block"}}> <div style={{display: "inline-block"}}>
@@ -113,6 +143,9 @@ export const CaptchaModal = (props) => {
const onChange = (token) => { const onChange = (token) => {
setCaptchaToken(token); setCaptchaToken(token);
if (noModal) {
onUpdateToken?.(captchaType, token, clientSecret);
}
}; };
const renderCaptcha = () => { const renderCaptcha = () => {
@@ -153,28 +186,33 @@ export const CaptchaModal = (props) => {
return null; return null;
}; };
return ( if (noModal) {
<Modal return renderCaptcha();
closable={true}
maskClosable={false} } else {
destroyOnClose={true} return (
title={i18next.t("general:Captcha")} <Modal
open={open} closable={true}
okText={i18next.t("general:OK")} maskClosable={false}
cancelText={i18next.t("general:Cancel")} destroyOnClose={true}
width={350} title={i18next.t("general:Captcha")}
footer={renderFooter()} open={open}
onCancel={handleCancel} okText={i18next.t("general:OK")}
afterClose={handleCancel} cancelText={i18next.t("general:Cancel")}
onOk={handleOk} width={350}
> footer={renderFooter()}
<div style={{marginTop: "20px", marginBottom: "50px"}}> onCancel={handleCancel}
{ afterClose={handleCancel}
renderCaptcha() onOk={handleOk}
} >
</div> <div style={{marginTop: "20px", marginBottom: "50px"}}>
</Modal> {
); renderCaptcha()
}
</div>
</Modal>
);
}
}; };
export const CaptchaRule = { export const CaptchaRule = {

View File

@@ -49,6 +49,9 @@ class SigninTable extends React.Component {
updateField(table, index, key, value) { updateField(table, index, key, value) {
table[index][key] = value; table[index][key] = value;
if (key === "name" && value === "Captcha") {
table[index]["rule"] = "pop up";
}
this.updateTable(table); this.updateTable(table);
} }
@@ -114,6 +117,7 @@ class SigninTable extends React.Component {
{name: "Forgot password?", displayName: i18next.t("login:Forgot password?")}, {name: "Forgot password?", displayName: i18next.t("login:Forgot password?")},
{name: "Login button", displayName: i18next.t("login:Signin button")}, {name: "Login button", displayName: i18next.t("login:Signin button")},
{name: "Signup link", displayName: i18next.t("general:Signup link")}, {name: "Signup link", displayName: i18next.t("general:Signup link")},
{name: "Captcha", displayName: i18next.t("general:Captcha")},
]; ];
const getItemDisplayName = (text) => { const getItemDisplayName = (text) => {
@@ -249,6 +253,12 @@ class SigninTable extends React.Component {
{id: "small", name: i18next.t("application:Small icon")}, {id: "small", name: i18next.t("application:Small icon")},
]; ];
} }
if (record.name === "Captcha") {
options = [
{id: "pop up", name: i18next.t("application:Pop up")},
{id: "inline", name: i18next.t("application:Inline")},
];
}
if (options.length === 0) { if (options.length === 0) {
return null; return null;
} }