diff --git a/web/src/ApplicationEditPage.js b/web/src/ApplicationEditPage.js index de0fef25..7ec4484e 100644 --- a/web/src/ApplicationEditPage.js +++ b/web/src/ApplicationEditPage.js @@ -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) => { diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js index 093d5dca..81d8a3ce 100644 --- a/web/src/auth/LoginPage.js +++ b/web/src/auth/LoginPage.js @@ -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")); @@ -877,6 +878,10 @@ class LoginPage extends React.Component { loginWidth += 10; } + if (this.state.loginMethod === "wechat") { + return (); + } + return (
{ @@ -1225,7 +1231,7 @@ class LoginPage extends React.Component { if (items.length > 1) { return (
- { + { this.setState({loginMethod: key}); }} centered> diff --git a/web/src/auth/WeChatLoginPanel.js b/web/src/auth/WeChatLoginPanel.js new file mode 100644 index 00000000..108bf3e1 --- /dev/null +++ b/web/src/auth/WeChatLoginPanel.js @@ -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, "login"); + }, 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 ( +
+ {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 ? ( +
+ {i18next.t("login:Loading...")} +
+ ) : qrCode ? ( + + ) : null} +
+ ); + } +} + +export default WeChatLoginPanel; diff --git a/web/src/table/SigninMethodTable.js b/web/src/table/SigninMethodTable.js index 2ae6a6a1..d03274da 100644 --- a/web/src/table/SigninMethodTable.js +++ b/web/src/table/SigninMethodTable.js @@ -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 = [ {