Compare commits

...

16 Commits

Author SHA1 Message Date
a92430e8fd feat: fix auto sign-in flow on result page (#3983) 2025-07-22 20:19:45 +08:00
447cb70553 feat: change some fields of organization and user to mediumtext 2025-07-21 23:43:17 +08:00
e05fbec739 feat: keep backward compatibility in GetHashedPassword() 2025-07-21 19:32:59 +08:00
65ab36f073 feat: fix bug that GetHashedPassword() reports error (#3982) 2025-07-21 14:41:09 +08:00
d027e07383 feat: fix bug that needUpdatePassword is not respected (#3979) 2025-07-21 10:17:24 +08:00
d3c718b577 feat: fix bug that language cannot be switched to user selected language (#3980) 2025-07-21 10:16:07 +08:00
ea68e6c2dc feat: support inline-captcha in login page (#3970) 2025-07-19 01:12:07 +08:00
7aa0b2e63f feat: change the method "login" to correct param "signup" (#3971) 2025-07-19 00:49:00 +08:00
a39b121280 feat: support WeChat login directly in login page (#3957) 2025-07-18 01:29:31 +08:00
feef4cc242 feat: set ResponseModesSupported to standard OIDC: "query", "fragment" (#3968) 2025-07-17 10:20:37 +08:00
1b5ef53655 feat: fix tour bug about orgIsTourVisible settings (#3965) 2025-07-16 18:00:44 +08:00
18d639cca2 feat: fix tour button (#3961) 2025-07-16 12:02:14 +08:00
3ac5aad648 feat: fix validate text error caused by password length check (#3964) 2025-07-16 10:10:13 +08:00
2a53241128 feat: support 15 more currencies (#3963) 2025-07-16 01:07:25 +08:00
835273576b feat: add Lark OAuth provider (#3956) 2025-07-13 19:51:45 +08:00
7fdc264ff6 feat: check if MFA is verified when required (#3954) 2025-07-12 15:20:44 +08:00
50 changed files with 854 additions and 96 deletions

View File

@ -286,8 +286,7 @@ func (c *ApiController) Signup() {
} }
} }
if application.HasPromptPage() && user.Type == "normal-user" { if user.Type == "normal-user" {
// The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId()) c.SetSessionUsername(user.GetId())
} }

View File

@ -38,9 +38,20 @@ func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
} }
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, salt string) string { func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, salt string) string {
if salt == "" {
return getMd5HexDigest(password)
}
return getMd5HexDigest(getMd5HexDigest(password) + salt) return getMd5HexDigest(getMd5HexDigest(password) + salt)
} }
func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool { func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
// For backward-compatibility
if salt == "" {
if hashedPwd == cm.GetHashedPassword(getMd5HexDigest(plainPwd), salt) {
return true
}
}
return hashedPwd == cm.GetHashedPassword(plainPwd, salt) return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
} }

View File

@ -38,9 +38,20 @@ func NewSha256SaltCredManager() *Sha256SaltCredManager {
} }
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, salt string) string { func (cm *Sha256SaltCredManager) GetHashedPassword(password string, salt string) string {
if salt == "" {
return getSha256HexDigest(password)
}
return getSha256HexDigest(getSha256HexDigest(password) + salt) return getSha256HexDigest(getSha256HexDigest(password) + salt)
} }
func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool { func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
// For backward-compatibility
if salt == "" {
if hashedPwd == cm.GetHashedPassword(getSha256HexDigest(plainPwd), salt) {
return true
}
}
return hashedPwd == cm.GetHashedPassword(plainPwd, salt) return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
} }

View File

@ -38,9 +38,20 @@ func NewSha512SaltCredManager() *Sha512SaltCredManager {
} }
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, salt string) string { func (cm *Sha512SaltCredManager) GetHashedPassword(password string, salt string) string {
if salt == "" {
return getSha512HexDigest(password)
}
return getSha512HexDigest(getSha512HexDigest(password) + salt) return getSha512HexDigest(getSha512HexDigest(password) + salt)
} }
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool { func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
// For backward-compatibility
if salt == "" {
if hashedPwd == cm.GetHashedPassword(getSha512HexDigest(plainPwd), salt) {
return true
}
}
return hashedPwd == cm.GetHashedPassword(plainPwd, salt) return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
} }

View File

@ -29,14 +29,20 @@ import (
type LarkIdProvider struct { type LarkIdProvider struct {
Client *http.Client Client *http.Client
Config *oauth2.Config Config *oauth2.Config
LarkDomain string
} }
func NewLarkIdProvider(clientId string, clientSecret string, redirectUrl string) *LarkIdProvider { func NewLarkIdProvider(clientId string, clientSecret string, redirectUrl string, useGlobalEndpoint bool) *LarkIdProvider {
idp := &LarkIdProvider{} idp := &LarkIdProvider{}
if useGlobalEndpoint {
idp.LarkDomain = "https://open.larksuite.com"
} else {
idp.LarkDomain = "https://open.feishu.cn"
}
config := idp.getConfig(clientId, clientSecret, redirectUrl) config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config idp.Config = config
return idp return idp
} }
@ -47,7 +53,7 @@ func (idp *LarkIdProvider) SetHttpClient(client *http.Client) {
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow // getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func (idp *LarkIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { func (idp *LarkIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
endpoint := oauth2.Endpoint{ endpoint := oauth2.Endpoint{
TokenURL: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", TokenURL: idp.LarkDomain + "/open-apis/auth/v3/tenant_access_token/internal",
} }
config := &oauth2.Config{ config := &oauth2.Config{
@ -162,6 +168,7 @@ type LarkUserInfo struct {
} `json:"data"` } `json:"data"`
} }
// GetUserInfo use LarkAccessToken gotten before return LinkedInUserInf
// GetUserInfo use LarkAccessToken gotten before return LinkedInUserInfo // GetUserInfo use LarkAccessToken gotten before return LinkedInUserInfo
// get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context // get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context
func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
@ -175,7 +182,7 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
return nil, err return nil, err
} }
req, err := http.NewRequest("POST", "https://open.feishu.cn/open-apis/authen/v1/access_token", strings.NewReader(string(data))) req, err := http.NewRequest("POST", idp.LarkDomain+"/open-apis/authen/v1/access_token", strings.NewReader(string(data)))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -87,7 +87,7 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType) return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType)
} }
case "Lark": case "Lark":
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
case "GitLab": case "GitLab":
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "ADFS": case "ADFS":

View File

@ -124,7 +124,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend), JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", 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"}, 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"}, GrantTypesSupported: []string{"password", "authorization_code"},
SubjectTypesSupported: []string{"public"}, SubjectTypesSupported: []string{"public"},
IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"}, IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},

View File

@ -81,12 +81,12 @@ type Organization struct {
UseEmailAsUsername bool `json:"useEmailAsUsername"` UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"` EnableTour bool `json:"enableTour"`
IpRestriction string `json:"ipRestriction"` IpRestriction string `json:"ipRestriction"`
NavItems []string `xorm:"varchar(1000)" json:"navItems"` NavItems []string `xorm:"mediumtext" json:"navItems"`
WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"` WidgetItems []string `xorm:"mediumtext" json:"widgetItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberInHours int `json:"mfaRememberInHours"` MfaRememberInHours int `json:"mfaRememberInHours"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"` AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
} }
func GetOrganizationCount(owner, name, field, value string) (int64, error) { func GetOrganizationCount(owner, name, field, value string) (int64, error) {

View File

@ -190,7 +190,7 @@ type User struct {
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"` WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"` PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes"` RecoveryCodes []string `xorm:"mediumtext" json:"recoveryCodes"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret"` TotpSecret string `xorm:"varchar(100)" json:"totpSecret"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"` MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"` MfaEmailEnabled bool `json:"mfaEmailEnabled"`
@ -204,7 +204,7 @@ type User struct {
Roles []*Role `json:"roles"` Roles []*Role `json:"roles"`
Permissions []*Permission `json:"permissions"` Permissions []*Permission `json:"permissions"`
Groups []string `xorm:"groups varchar(1000)" json:"groups"` Groups []string `xorm:"mediumtext" json:"groups"`
LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"` LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`

View File

@ -247,7 +247,9 @@ class App extends Component {
account.organization = res.data2; account.organization = res.data2;
accessToken = res.data.accessToken; accessToken = res.data.accessToken;
if (!localStorage.getItem("language")) {
this.setLanguage(account); this.setLanguage(account);
}
this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm); this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm);
setTourLogo(account.organization.logo); setTourLogo(account.organization.logo);
setOrgIsTourVisible(account.organization.enableTour); setOrgIsTourVisible(account.organization.enableTour);
@ -404,6 +406,7 @@ class App extends Component {
account={this.state.account} account={this.state.account}
theme={this.state.themeData} theme={this.state.themeData}
themeAlgorithm={this.state.themeAlgorithm} themeAlgorithm={this.state.themeAlgorithm}
requiredEnableMfa={this.state.requiredEnableMfa}
updateApplication={(application) => { updateApplication={(application) => {
this.setState({ this.setState({
application: application, application: application,

View File

@ -1237,7 +1237,7 @@ class ApplicationEditPage extends React.Component {
submitApplicationEdit(exitAfterSave) { submitApplicationEdit(exitAfterSave) {
const application = Setting.deepCopy(this.state.application); const application = Setting.deepCopy(this.state.application);
application.providers = application.providers?.filter(provider => this.state.providers.map(provider => provider.name).includes(provider.name)); 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) ApplicationBackend.updateApplication("admin", this.state.applicationName, application)
.then((res) => { .then((res) => {

View File

@ -241,6 +241,21 @@ class PlanEditPage extends React.Component {
{id: "HKD", name: "HKD"}, {id: "HKD", name: "HKD"},
{id: "SGD", name: "SGD"}, {id: "SGD", name: "SGD"},
{id: "BRL", name: "BRL"}, {id: "BRL", name: "BRL"},
{id: "PLN", name: "PLN"},
{id: "KRW", name: "KRW"},
{id: "INR", name: "INR"},
{id: "RUB", name: "RUB"},
{id: "MXN", name: "MXN"},
{id: "ZAR", name: "ZAR"},
{id: "TRY", name: "TRY"},
{id: "SEK", name: "SEK"},
{id: "NOK", name: "NOK"},
{id: "DKK", name: "DKK"},
{id: "THB", name: "THB"},
{id: "MYR", name: "MYR"},
{id: "TWD", name: "TWD"},
{id: "CZK", name: "CZK"},
{id: "HUF", name: "HUF"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>

View File

@ -141,6 +141,36 @@ class ProductBuyPage extends React.Component {
return "S$"; return "S$";
} else if (product?.currency === "BRL") { } else if (product?.currency === "BRL") {
return "R$"; return "R$";
} else if (product?.currency === "PLN") {
return "zł";
} else if (product?.currency === "KRW") {
return "₩";
} else if (product?.currency === "INR") {
return "₹";
} else if (product?.currency === "RUB") {
return "₽";
} else if (product?.currency === "MXN") {
return "$";
} else if (product?.currency === "ZAR") {
return "R";
} else if (product?.currency === "TRY") {
return "₺";
} else if (product?.currency === "SEK") {
return "kr";
} else if (product?.currency === "NOK") {
return "kr";
} else if (product?.currency === "DKK") {
return "kr";
} else if (product?.currency === "THB") {
return "฿";
} else if (product?.currency === "MYR") {
return "RM";
} else if (product?.currency === "TWD") {
return "NT$";
} else if (product?.currency === "CZK") {
return "Kč";
} else if (product?.currency === "HUF") {
return "Ft";
} else { } else {
return "(Unknown currency)"; return "(Unknown currency)";
} }

View File

@ -218,6 +218,21 @@ class ProductEditPage extends React.Component {
{id: "HKD", name: "HKD"}, {id: "HKD", name: "HKD"},
{id: "SGD", name: "SGD"}, {id: "SGD", name: "SGD"},
{id: "BRL", name: "BRL"}, {id: "BRL", name: "BRL"},
{id: "PLN", name: "PLN"},
{id: "KRW", name: "KRW"},
{id: "INR", name: "INR"},
{id: "RUB", name: "RUB"},
{id: "MXN", name: "MXN"},
{id: "ZAR", name: "ZAR"},
{id: "TRY", name: "TRY"},
{id: "SEK", name: "SEK"},
{id: "NOK", name: "NOK"},
{id: "DKK", name: "DKK"},
{id: "THB", name: "THB"},
{id: "MYR", name: "MYR"},
{id: "TWD", name: "TWD"},
{id: "CZK", name: "CZK"},
{id: "HUF", name: "HUF"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>

View File

@ -931,10 +931,12 @@ class ProviderEditPage extends React.Component {
) )
} }
{ {
this.state.provider.type !== "Google" ? null : ( this.state.provider.type !== "Google" && this.state.provider.type !== "Lark" ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Get phone number"), i18next.t("provider:Get phone number - Tooltip"))} : {this.state.provider.type === "Google" ?
Setting.getLabel(i18next.t("provider:Get phone number"), i18next.t("provider:Get phone number - Tooltip"))
: Setting.getLabel(i18next.t("provider:Use global endpoint"), i18next.t("provider:Use global endpoint - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch disabled={!this.state.provider.clientId} checked={this.state.provider.disableSsl} onChange={checked => { <Switch disabled={!this.state.provider.clientId} checked={this.state.provider.disableSsl} onChange={checked => {

View File

@ -1516,6 +1516,54 @@ export function getCurrencySymbol(currency) {
return "$"; return "$";
} else if (currency === "CNY" || currency === "cny") { } else if (currency === "CNY" || currency === "cny") {
return "¥"; return "¥";
} else if (currency === "EUR" || currency === "eur") {
return "€";
} else if (currency === "JPY" || currency === "jpy") {
return "¥";
} else if (currency === "GBP" || currency === "gbp") {
return "£";
} else if (currency === "AUD" || currency === "aud") {
return "A$";
} else if (currency === "CAD" || currency === "cad") {
return "C$";
} else if (currency === "CHF" || currency === "chf") {
return "CHF";
} else if (currency === "HKD" || currency === "hkd") {
return "HK$";
} else if (currency === "SGD" || currency === "sgd") {
return "S$";
} else if (currency === "BRL" || currency === "brl") {
return "R$";
} else if (currency === "PLN" || currency === "pln") {
return "zł";
} else if (currency === "KRW" || currency === "krw") {
return "₩";
} else if (currency === "INR" || currency === "inr") {
return "₹";
} else if (currency === "RUB" || currency === "rub") {
return "₽";
} else if (currency === "MXN" || currency === "mxn") {
return "$";
} else if (currency === "ZAR" || currency === "zar") {
return "R";
} else if (currency === "TRY" || currency === "try") {
return "₺";
} else if (currency === "SEK" || currency === "sek") {
return "kr";
} else if (currency === "NOK" || currency === "nok") {
return "kr";
} else if (currency === "DKK" || currency === "dkk") {
return "kr";
} else if (currency === "THB" || currency === "thb") {
return "฿";
} else if (currency === "MYR" || currency === "myr") {
return "RM";
} else if (currency === "TWD" || currency === "twd") {
return "NT$";
} else if (currency === "CZK" || currency === "czk") {
return "Kč";
} else if (currency === "HUF" || currency === "huf") {
return "Ft";
} else { } else {
return currency; return currency;
} }
@ -1619,6 +1667,36 @@ export function getCurrencyText(product) {
return i18next.t("currency:SGD"); return i18next.t("currency:SGD");
} else if (product?.currency === "BRL") { } else if (product?.currency === "BRL") {
return i18next.t("currency:BRL"); return i18next.t("currency:BRL");
} else if (product?.currency === "PLN") {
return i18next.t("currency:PLN");
} else if (product?.currency === "KRW") {
return i18next.t("currency:KRW");
} else if (product?.currency === "INR") {
return i18next.t("currency:INR");
} else if (product?.currency === "RUB") {
return i18next.t("currency:RUB");
} else if (product?.currency === "MXN") {
return i18next.t("currency:MXN");
} else if (product?.currency === "ZAR") {
return i18next.t("currency:ZAR");
} else if (product?.currency === "TRY") {
return i18next.t("currency:TRY");
} else if (product?.currency === "SEK") {
return i18next.t("currency:SEK");
} else if (product?.currency === "NOK") {
return i18next.t("currency:NOK");
} else if (product?.currency === "DKK") {
return i18next.t("currency:DKK");
} else if (product?.currency === "THB") {
return i18next.t("currency:THB");
} else if (product?.currency === "MYR") {
return i18next.t("currency:MYR");
} else if (product?.currency === "TWD") {
return i18next.t("currency:TWD");
} else if (product?.currency === "CZK") {
return i18next.t("currency:CZK");
} else if (product?.currency === "HUF") {
return i18next.t("currency:HUF");
} else { } else {
return "(Unknown currency)"; return "(Unknown currency)";
} }

View File

@ -208,10 +208,14 @@ let orgIsTourVisible = true;
export function setOrgIsTourVisible(visible) { export function setOrgIsTourVisible(visible) {
orgIsTourVisible = visible; orgIsTourVisible = visible;
if (orgIsTourVisible === false) {
setIsTourVisible(false);
}
} }
export function setIsTourVisible(visible) { export function setIsTourVisible(visible) {
localStorage.setItem("isTourVisible", visible); localStorage.setItem("isTourVisible", visible);
window.dispatchEvent(new Event("storageTourChanged"));
} }
export function setTourLogo(tourLogoSrc) { export function setTourLogo(tourLogoSrc) {
@ -221,7 +225,7 @@ export function setTourLogo(tourLogoSrc) {
} }
export function getTourVisible() { export function getTourVisible() {
return localStorage.getItem("isTourVisible") !== "false" && orgIsTourVisible; return localStorage.getItem("isTourVisible") !== "false";
} }
export function getNextButtonChild(nextPathName) { export function getNextButtonChild(nextPathName) {

View File

@ -38,6 +38,7 @@ import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton"; import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
import * as ProviderButton from "./ProviderButton"; import * as ProviderButton from "./ProviderButton";
import {goToLink} from "../Setting"; import {goToLink} from "../Setting";
import WeChatLoginPanel from "./WeChatLoginPanel";
const FaceRecognitionCommonModal = lazy(() => import("../common/modal/FaceRecognitionCommonModal")); const FaceRecognitionCommonModal = lazy(() => import("../common/modal/FaceRecognitionCommonModal"));
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal")); const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
@ -346,7 +347,7 @@ class LoginPage extends React.Component {
return; return;
} }
if (resp.data2) { if (resp.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search); sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLinkSoft(ths, `/forget/${application.name}`); Setting.goToLinkSoft(ths, `/forget/${application.name}`);
return; return;
@ -436,6 +437,9 @@ class LoginPage extends React.Component {
values["password"] = passwordCipher; values["password"] = passwordCipher;
} }
const captchaRule = this.getCaptchaRule(this.getApplicationObj()); const captchaRule = this.getCaptchaRule(this.getApplicationObj());
const application = this.getApplicationObj();
const noModal = application?.signinItems.map(signinItem => signinItem.name === "Captcha" && signinItem.rule === "inline").includes(true);
if (!noModal) {
if (captchaRule === CaptchaRule.Always) { if (captchaRule === CaptchaRule.Always) {
this.setState({ this.setState({
openCaptchaModal: true, openCaptchaModal: true,
@ -449,6 +453,11 @@ class LoginPage extends React.Component {
this.checkCaptchaStatus(values); this.checkCaptchaStatus(values);
return; 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);
} }
@ -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> </Form.Item>
); );
@ -818,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}} />
@ -877,6 +888,10 @@ class LoginPage extends React.Component {
loginWidth += 10; 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 ( return (
<Form <Form
name="normal_login" name="normal_login"
@ -959,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;
} }
@ -988,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;
@ -1040,6 +1061,10 @@ class LoginPage extends React.Component {
return null; return null;
} }
if (this.props.requiredEnableMfa) {
return null;
}
if (this.state.userCode && this.state.userCodeStatus === "success") { if (this.state.userCode && this.state.userCodeStatus === "success") {
return null; return null;
} }
@ -1200,6 +1225,7 @@ class LoginPage extends React.Component {
[generateItemKey("WebAuthn", "None"), {label: i18next.t("login:WebAuthn"), key: "webAuthn"}], [generateItemKey("WebAuthn", "None"), {label: i18next.t("login:WebAuthn"), key: "webAuthn"}],
[generateItemKey("LDAP", "None"), {label: i18next.t("login:LDAP"), key: "ldap"}], [generateItemKey("LDAP", "None"), {label: i18next.t("login:LDAP"), key: "ldap"}],
[generateItemKey("Face ID", "None"), {label: i18next.t("login:Face ID"), key: "faceId"}], [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) => { application?.signinMethods?.forEach((signinMethod) => {
@ -1221,7 +1247,7 @@ class LoginPage extends React.Component {
if (items.length > 1) { if (items.length > 1) {
return ( return (
<div> <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}); this.setState({loginMethod: key});
}} centered> }} centered>
</Tabs> </Tabs>

View File

@ -68,6 +68,7 @@ const authInfo = {
Lark: { Lark: {
// scope: "email", // scope: "email",
endpoint: "https://open.feishu.cn/open-apis/authen/v1/index", endpoint: "https://open.feishu.cn/open-apis/authen/v1/index",
endpoint2: "https://accounts.larksuite.com/open-apis/authen/v1/authorize",
}, },
GitLab: { GitLab: {
scope: "read_user+profile", scope: "read_user+profile",
@ -406,6 +407,8 @@ export function getAuthUrl(application, provider, method, code) {
if (provider.domain) { if (provider.domain) {
endpoint = `${provider.domain}/apps/oauth2/authorize`; endpoint = `${provider.domain}/apps/oauth2/authorize`;
} }
} else if (provider.type === "Lark" && provider.disableSsl) {
endpoint = authInfo[provider.type].endpoint2;
} }
if (provider.type === "Google" || provider.type === "GitHub" || provider.type === "Facebook" if (provider.type === "Google" || provider.type === "GitHub" || provider.type === "Facebook"
@ -460,6 +463,9 @@ export function getAuthUrl(application, provider, method, code) {
return `https://error:not-supported-provider-sub-type:${provider.subType}`; return `https://error:not-supported-provider-sub-type:${provider.subType}`;
} }
} else if (provider.type === "Lark") { } else if (provider.type === "Lark") {
if (provider.disableSsl) {
redirectUri = encodeURIComponent(redirectUri);
}
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`; return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`;
} else if (provider.type === "ADFS") { } else if (provider.type === "ADFS") {
return `${provider.domain}/adfs/oauth2/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&nonce=casdoor&scope=openid`; return `${provider.domain}/adfs/oauth2/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&nonce=casdoor&scope=openid`;

View File

@ -18,6 +18,7 @@ import i18next from "i18next";
import {authConfig} from "./Auth"; import {authConfig} from "./Auth";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend";
class ResultPage extends React.Component { class ResultPage extends React.Component {
constructor(props) { constructor(props) {
@ -60,6 +61,22 @@ class ResultPage extends React.Component {
this.props.onUpdateApplication(application); this.props.onUpdateApplication(application);
} }
handleSignIn = () => {
AuthBackend.getAccount()
.then((res) => {
if (res.status === "ok" && res.data) {
const linkInStorage = sessionStorage.getItem("signinUrl");
if (linkInStorage !== null && linkInStorage !== "") {
window.location.href = linkInStorage;
} else {
Setting.goToLink("/");
}
} else {
Setting.redirectToLoginPage(this.state.application, this.props.history);
}
});
};
render() { render() {
const application = this.state.application; const application = this.state.application;
@ -89,14 +106,7 @@ class ResultPage extends React.Component {
title={i18next.t("signup:Your account has been created!")} title={i18next.t("signup:Your account has been created!")}
subTitle={i18next.t("signup:Please click the below button to sign in")} subTitle={i18next.t("signup:Please click the below button to sign in")}
extra={[ extra={[
<Button type="primary" key="login" onClick={() => { <Button type="primary" key="login" onClick={this.handleSignIn}>
const linkInStorage = sessionStorage.getItem("signinUrl");
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLinkSoft(this, linkInStorage);
} else {
Setting.redirectToLoginPage(application, this.props.history);
}
}}>
{i18next.t("login:Sign In")} {i18next.t("login:Sign In")}
</Button>, </Button>,
]} ]}

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) { export function checkPasswordComplexity(password, options) {
if (password.length === 0) { if (!password?.length) {
return i18next.t("login:Please input your password!"); return i18next.t("login:Please input your password!");
} }

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,6 +186,10 @@ export const CaptchaModal = (props) => {
return null; return null;
}; };
if (noModal) {
return renderCaptcha();
} else {
return ( return (
<Modal <Modal
closable={true} closable={true}
@ -175,6 +212,7 @@ export const CaptchaModal = (props) => {
</div> </div>
</Modal> </Modal>
); );
}
}; };
export const CaptchaRule = { export const CaptchaRule = {

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Upravit Enforcer", "Edit Enforcer": "Upravit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "ویرایش Enforcer", "Edit Enforcer": "ویرایش Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Éditer l'exécuteur", "Edit Enforcer": "Éditer l'exécuteur",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Editar Executor", "Edit Enforcer": "Editar Executor",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Редактировать контролёра доступа", "Edit Enforcer": "Редактировать контролёра доступа",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Upraviť vynútiteľa", "Edit Enforcer": "Upraviť vynútiteľa",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Редагувати Enforcer", "Edit Enforcer": "Редагувати Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR", "EUR": "EUR",
"GBP": "GBP", "GBP": "GBP",
"HKD": "HKD", "HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY", "JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD", "SGD": "SGD",
"USD": "USD" "THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "Edit Enforcer", "Edit Enforcer": "Edit Enforcer",

View File

@ -177,12 +177,27 @@
"CAD": "加拿大元", "CAD": "加拿大元",
"CHF": "瑞士法郎", "CHF": "瑞士法郎",
"CNY": "人民币", "CNY": "人民币",
"CZK": "捷克克朗",
"DKK": "丹麦克朗",
"EUR": "欧元", "EUR": "欧元",
"GBP": "英镑", "GBP": "英镑",
"HKD": "港币", "HKD": "港币",
"HUF": "匈牙利福林",
"INR": "印度卢比",
"JPY": "日元", "JPY": "日元",
"KRW": "韩元",
"MXN": "墨西哥比索",
"MYR": "马来西亚林吉特",
"NOK": "挪威克朗",
"PLN": "波兰兹罗提",
"RUB": "俄罗斯卢布",
"SEK": "瑞典克朗",
"SGD": "新加坡元", "SGD": "新加坡元",
"USD": "美元" "THB": "泰铢",
"TRY": "土耳其里拉",
"TWD": "新台币",
"USD": "美元",
"ZAR": "南非兰特"
}, },
"enforcer": { "enforcer": {
"Edit Enforcer": "编辑Casbin执行器", "Edit Enforcer": "编辑Casbin执行器",

View File

@ -72,6 +72,7 @@ class SigninMethodTable extends React.Component {
{name: "WebAuthn", displayName: i18next.t("login:WebAuthn")}, {name: "WebAuthn", displayName: i18next.t("login:WebAuthn")},
{name: "LDAP", displayName: i18next.t("login:LDAP")}, {name: "LDAP", displayName: i18next.t("login:LDAP")},
{name: "Face ID", displayName: i18next.t("login:Face ID")}, {name: "Face ID", displayName: i18next.t("login:Face ID")},
{name: "WeChat", displayName: i18next.t("login:WeChat")},
]; ];
const columns = [ const columns = [
{ {

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