mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-23 22:53:31 +08:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
ea68e6c2dc | |||
7aa0b2e63f | |||
a39b121280 | |||
feef4cc242 | |||
1b5ef53655 | |||
18d639cca2 |
@ -124,7 +124,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
||||
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
|
||||
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
|
||||
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
|
||||
ResponseModesSupported: []string{"query", "fragment", "login", "code", "link"},
|
||||
ResponseModesSupported: []string{"query", "fragment"},
|
||||
GrantTypesSupported: []string{"password", "authorization_code"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},
|
||||
|
@ -1237,7 +1237,7 @@ class ApplicationEditPage extends React.Component {
|
||||
submitApplicationEdit(exitAfterSave) {
|
||||
const application = Setting.deepCopy(this.state.application);
|
||||
application.providers = application.providers?.filter(provider => this.state.providers.map(provider => provider.name).includes(provider.name));
|
||||
application.signinMethods = application.signinMethods?.filter(signinMethod => ["Password", "Verification code", "WebAuthn", "LDAP", "Face ID"].includes(signinMethod.name));
|
||||
application.signinMethods = application.signinMethods?.filter(signinMethod => ["Password", "Verification code", "WebAuthn", "LDAP", "Face ID", "WeChat"].includes(signinMethod.name));
|
||||
|
||||
ApplicationBackend.updateApplication("admin", this.state.applicationName, application)
|
||||
.then((res) => {
|
||||
|
@ -208,10 +208,14 @@ let orgIsTourVisible = true;
|
||||
|
||||
export function setOrgIsTourVisible(visible) {
|
||||
orgIsTourVisible = visible;
|
||||
if (orgIsTourVisible === false) {
|
||||
setIsTourVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function setIsTourVisible(visible) {
|
||||
localStorage.setItem("isTourVisible", visible);
|
||||
window.dispatchEvent(new Event("storageTourChanged"));
|
||||
}
|
||||
|
||||
export function setTourLogo(tourLogoSrc) {
|
||||
@ -221,7 +225,7 @@ export function setTourLogo(tourLogoSrc) {
|
||||
}
|
||||
|
||||
export function getTourVisible() {
|
||||
return localStorage.getItem("isTourVisible") !== "false" && orgIsTourVisible;
|
||||
return localStorage.getItem("isTourVisible") !== "false";
|
||||
}
|
||||
|
||||
export function getNextButtonChild(nextPathName) {
|
||||
|
@ -38,6 +38,7 @@ import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
import {goToLink} from "../Setting";
|
||||
import WeChatLoginPanel from "./WeChatLoginPanel";
|
||||
const FaceRecognitionCommonModal = lazy(() => import("../common/modal/FaceRecognitionCommonModal"));
|
||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||
|
||||
@ -436,18 +437,26 @@ class LoginPage extends React.Component {
|
||||
values["password"] = passwordCipher;
|
||||
}
|
||||
const captchaRule = this.getCaptchaRule(this.getApplicationObj());
|
||||
if (captchaRule === CaptchaRule.Always) {
|
||||
this.setState({
|
||||
openCaptchaModal: true,
|
||||
values: values,
|
||||
});
|
||||
return;
|
||||
} else if (captchaRule === CaptchaRule.Dynamic) {
|
||||
this.checkCaptchaStatus(values);
|
||||
return;
|
||||
} else if (captchaRule === CaptchaRule.InternetOnly) {
|
||||
this.checkCaptchaStatus(values);
|
||||
return;
|
||||
const application = this.getApplicationObj();
|
||||
const noModal = application?.signinItems.map(signinItem => signinItem.name === "Captcha" && signinItem.rule === "inline").includes(true);
|
||||
if (!noModal) {
|
||||
if (captchaRule === CaptchaRule.Always) {
|
||||
this.setState({
|
||||
openCaptchaModal: true,
|
||||
values: values,
|
||||
});
|
||||
return;
|
||||
} else if (captchaRule === CaptchaRule.Dynamic) {
|
||||
this.checkCaptchaStatus(values);
|
||||
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);
|
||||
@ -774,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>
|
||||
);
|
||||
@ -818,6 +827,8 @@ class LoginPage extends React.Component {
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
} else if (signinItem.name === "Captcha" && signinItem.rule === "inline") {
|
||||
return this.renderCaptchaModal(application, true);
|
||||
} else if (signinItem.name.startsWith("Text ") || signinItem?.isCustom) {
|
||||
return (
|
||||
<div key={resultItemKey} dangerouslySetInnerHTML={{__html: signinItem.customCss}} />
|
||||
@ -877,6 +888,10 @@ class LoginPage extends React.Component {
|
||||
loginWidth += 10;
|
||||
}
|
||||
|
||||
if (this.state.loginMethod === "wechat") {
|
||||
return (<WeChatLoginPanel application={application} renderFormItem={this.renderFormItem.bind(this)} loginMethod={this.state.loginMethod} loginWidth={loginWidth} renderMethodChoiceBox={this.renderMethodChoiceBox.bind(this)} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
name="normal_login"
|
||||
@ -959,7 +974,7 @@ class LoginPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
renderCaptchaModal(application) {
|
||||
renderCaptchaModal(application, noModal) {
|
||||
if (this.getCaptchaRule(this.getApplicationObj()) === CaptchaRule.Never) {
|
||||
return null;
|
||||
}
|
||||
@ -988,6 +1003,12 @@ class LoginPage extends React.Component {
|
||||
owner={provider.owner}
|
||||
name={provider.name}
|
||||
visible={this.state.openCaptchaModal}
|
||||
noModal={noModal}
|
||||
onUpdateToken={(captchaType, captchaToken, clientSecret) => {
|
||||
this.setState({captchaValues: {
|
||||
captchaType, captchaToken, clientSecret,
|
||||
}});
|
||||
}}
|
||||
onOk={(captchaType, captchaToken, clientSecret) => {
|
||||
const values = this.state.values;
|
||||
values["captchaType"] = captchaType;
|
||||
@ -1204,6 +1225,7 @@ class LoginPage extends React.Component {
|
||||
[generateItemKey("WebAuthn", "None"), {label: i18next.t("login:WebAuthn"), key: "webAuthn"}],
|
||||
[generateItemKey("LDAP", "None"), {label: i18next.t("login:LDAP"), key: "ldap"}],
|
||||
[generateItemKey("Face ID", "None"), {label: i18next.t("login:Face ID"), key: "faceId"}],
|
||||
[generateItemKey("WeChat", "None"), {label: i18next.t("login:WeChat"), key: "wechat"}],
|
||||
]);
|
||||
|
||||
application?.signinMethods?.forEach((signinMethod) => {
|
||||
@ -1225,7 +1247,7 @@ class LoginPage extends React.Component {
|
||||
if (items.length > 1) {
|
||||
return (
|
||||
<div>
|
||||
<Tabs className="signin-methods" items={items} size={"small"} defaultActiveKey={this.getDefaultLoginMethod(application)} onChange={(key) => {
|
||||
<Tabs className="signin-methods" items={items} size={"small"} activeKey={this.state.loginMethod} onChange={(key) => {
|
||||
this.setState({loginMethod: key});
|
||||
}} centered>
|
||||
</Tabs>
|
||||
|
106
web/src/auth/WeChatLoginPanel.js
Normal file
106
web/src/auth/WeChatLoginPanel.js
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import i18next from "i18next";
|
||||
import * as Util from "./Util";
|
||||
|
||||
class WeChatLoginPanel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
qrCode: null,
|
||||
loading: false,
|
||||
ticket: null,
|
||||
};
|
||||
this.pollingTimer = null;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.fetchQrCode();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.loginMethod === "wechat" && prevProps.loginMethod !== "wechat") {
|
||||
this.fetchQrCode();
|
||||
}
|
||||
if (prevProps.loginMethod === "wechat" && this.props.loginMethod !== "wechat") {
|
||||
this.setState({qrCode: null, loading: false, ticket: null});
|
||||
this.clearPolling();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearPolling();
|
||||
}
|
||||
|
||||
clearPolling() {
|
||||
if (this.pollingTimer) {
|
||||
clearInterval(this.pollingTimer);
|
||||
this.pollingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
fetchQrCode() {
|
||||
const {application} = this.props;
|
||||
const wechatProviderItem = application?.providers?.find(p => p.provider?.type === "WeChat");
|
||||
if (wechatProviderItem) {
|
||||
this.setState({loading: true, qrCode: null, ticket: null});
|
||||
AuthBackend.getWechatQRCode(`${wechatProviderItem.provider.owner}/${wechatProviderItem.provider.name}`).then(res => {
|
||||
if (res.status === "ok" && res.data) {
|
||||
this.setState({qrCode: res.data, loading: false, ticket: res.data2});
|
||||
this.clearPolling();
|
||||
this.pollingTimer = setInterval(() => {
|
||||
Util.getEvent(application, wechatProviderItem.provider, res.data2, "signup");
|
||||
}, 1000);
|
||||
} else {
|
||||
this.setState({qrCode: null, loading: false, ticket: null});
|
||||
this.clearPolling();
|
||||
}
|
||||
}).catch(() => {
|
||||
this.setState({qrCode: null, loading: false, ticket: null});
|
||||
this.clearPolling();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {application, loginWidth = 320} = this.props;
|
||||
const {loading, qrCode} = this.state;
|
||||
return (
|
||||
<div style={{width: loginWidth, margin: "0 auto", textAlign: "center", marginTop: 16}}>
|
||||
{application.signinItems?.filter(item => item.name === "Logo").map(signinItem => this.props.renderFormItem(application, signinItem))}
|
||||
{this.props.renderMethodChoiceBox()}
|
||||
{application.signinItems?.filter(item => item.name === "Languages").map(signinItem => this.props.renderFormItem(application, signinItem))}
|
||||
{loading ? (
|
||||
<div style={{marginTop: 16}}>
|
||||
<span>{i18next.t("login:Loading...")}</span>
|
||||
</div>
|
||||
) : qrCode ? (
|
||||
<div style={{marginTop: 2}}>
|
||||
<img src={`data:image/png;base64,${qrCode}`} alt="WeChat QR code" style={{width: 250, height: 250}} />
|
||||
<div style={{marginTop: 8}}>
|
||||
<a onClick={e => {e.preventDefault(); this.fetchQrCode();}}>
|
||||
{i18next.t("login:Refresh")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WeChatLoginPanel;
|
@ -20,7 +20,7 @@ import {CaptchaWidget} from "../CaptchaWidget";
|
||||
import {SafetyOutlined} from "@ant-design/icons";
|
||||
|
||||
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 [clientId, setClientId] = React.useState("");
|
||||
@ -36,16 +36,16 @@ export const CaptchaModal = (props) => {
|
||||
const defaultInputRef = React.useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (visible || noModal) {
|
||||
loadCaptcha();
|
||||
} else {
|
||||
handleCancel();
|
||||
setOpen(false);
|
||||
}
|
||||
}, [visible]);
|
||||
}, [visible, noModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (captchaToken !== "" && captchaType !== "Default") {
|
||||
if (captchaToken !== "" && captchaType !== "Default" && !noModal) {
|
||||
handleOk();
|
||||
}
|
||||
}, [captchaToken]);
|
||||
@ -81,6 +81,36 @@ export const CaptchaModal = (props) => {
|
||||
};
|
||||
|
||||
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 (
|
||||
<Col style={{textAlign: "center"}}>
|
||||
<div style={{display: "inline-block"}}>
|
||||
@ -113,6 +143,9 @@ export const CaptchaModal = (props) => {
|
||||
|
||||
const onChange = (token) => {
|
||||
setCaptchaToken(token);
|
||||
if (noModal) {
|
||||
onUpdateToken?.(captchaType, token, clientSecret);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCaptcha = () => {
|
||||
@ -153,28 +186,33 @@ export const CaptchaModal = (props) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable={true}
|
||||
maskClosable={false}
|
||||
destroyOnClose={true}
|
||||
title={i18next.t("general:Captcha")}
|
||||
open={open}
|
||||
okText={i18next.t("general:OK")}
|
||||
cancelText={i18next.t("general:Cancel")}
|
||||
width={350}
|
||||
footer={renderFooter()}
|
||||
onCancel={handleCancel}
|
||||
afterClose={handleCancel}
|
||||
onOk={handleOk}
|
||||
>
|
||||
<div style={{marginTop: "20px", marginBottom: "50px"}}>
|
||||
{
|
||||
renderCaptcha()
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
if (noModal) {
|
||||
return renderCaptcha();
|
||||
|
||||
} else {
|
||||
return (
|
||||
<Modal
|
||||
closable={true}
|
||||
maskClosable={false}
|
||||
destroyOnClose={true}
|
||||
title={i18next.t("general:Captcha")}
|
||||
open={open}
|
||||
okText={i18next.t("general:OK")}
|
||||
cancelText={i18next.t("general:Cancel")}
|
||||
width={350}
|
||||
footer={renderFooter()}
|
||||
onCancel={handleCancel}
|
||||
afterClose={handleCancel}
|
||||
onOk={handleOk}
|
||||
>
|
||||
<div style={{marginTop: "20px", marginBottom: "50px"}}>
|
||||
{
|
||||
renderCaptcha()
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const CaptchaRule = {
|
||||
|
@ -72,6 +72,7 @@ class SigninMethodTable extends React.Component {
|
||||
{name: "WebAuthn", displayName: i18next.t("login:WebAuthn")},
|
||||
{name: "LDAP", displayName: i18next.t("login:LDAP")},
|
||||
{name: "Face ID", displayName: i18next.t("login:Face ID")},
|
||||
{name: "WeChat", displayName: i18next.t("login:WeChat")},
|
||||
];
|
||||
const columns = [
|
||||
{
|
||||
|
@ -49,6 +49,9 @@ class SigninTable extends React.Component {
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
if (key === "name" && value === "Captcha") {
|
||||
table[index]["rule"] = "pop up";
|
||||
}
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
@ -114,6 +117,7 @@ class SigninTable extends React.Component {
|
||||
{name: "Forgot password?", displayName: i18next.t("login:Forgot password?")},
|
||||
{name: "Login button", displayName: i18next.t("login:Signin button")},
|
||||
{name: "Signup link", displayName: i18next.t("general:Signup link")},
|
||||
{name: "Captcha", displayName: i18next.t("general:Captcha")},
|
||||
];
|
||||
|
||||
const getItemDisplayName = (text) => {
|
||||
@ -249,6 +253,12 @@ class SigninTable extends React.Component {
|
||||
{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) {
|
||||
return null;
|
||||
}
|
||||
|
Reference in New Issue
Block a user