Compare commits

...

6 Commits

7 changed files with 122 additions and 5 deletions

View File

@ -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"},

View File

@ -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) => {

View File

@ -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) {

View File

@ -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 (<WeChatLoginPanel application={application} renderFormItem={this.renderFormItem.bind(this)} loginMethod={this.state.loginMethod} loginWidth={loginWidth} renderMethodChoiceBox={this.renderMethodChoiceBox.bind(this)} />);
}
return (
<Form
name="normal_login"
@ -1204,6 +1209,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 +1231,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>

View 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;

View File

@ -82,7 +82,7 @@ export function renderPasswordPopover(options, password) {
}
export function checkPasswordComplexity(password, options) {
if (password.length === 0) {
if (!password?.length) {
return i18next.t("login:Please input your password!");
}

View File

@ -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 = [
{