Compare commits

...

38 Commits

Author SHA1 Message Date
DacongDA
5f702ca418 feat: make enableErrorMask work for corner cases by moving checks from controller to Translate() (#3996) 2025-07-25 00:39:01 +08:00
Robin Ye
0495d17a07 feat: support OAuth 2.0 form_post response mode (#3973) 2025-07-24 15:17:45 +08:00
Yang Luo
c6a2d59aa4 feat: update i18n strings 2025-07-24 15:15:19 +08:00
DacongDA
d867afdd70 feat: can set default value for "Auto sign in" in application edit page (#3987) 2025-07-22 22:57:01 +08:00
Attack825
a92430e8fd feat: fix auto sign-in flow on result page (#3983) 2025-07-22 20:19:45 +08:00
Yang Luo
447cb70553 feat: change some fields of organization and user to mediumtext 2025-07-21 23:43:17 +08:00
Yang Luo
e05fbec739 feat: keep backward compatibility in GetHashedPassword() 2025-07-21 19:32:59 +08:00
DacongDA
65ab36f073 feat: fix bug that GetHashedPassword() reports error (#3982) 2025-07-21 14:41:09 +08:00
M Zahid Rausyanfikri
d027e07383 feat: fix bug that needUpdatePassword is not respected (#3979) 2025-07-21 10:17:24 +08:00
DacongDA
d3c718b577 feat: fix bug that language cannot be switched to user selected language (#3980) 2025-07-21 10:16:07 +08:00
DacongDA
ea68e6c2dc feat: support inline-captcha in login page (#3970) 2025-07-19 01:12:07 +08:00
raiki02
7aa0b2e63f feat: change the method "login" to correct param "signup" (#3971) 2025-07-19 00:49:00 +08:00
raiki02
a39b121280 feat: support WeChat login directly in login page (#3957) 2025-07-18 01:29:31 +08:00
DacongDA
feef4cc242 feat: set ResponseModesSupported to standard OIDC: "query", "fragment" (#3968) 2025-07-17 10:20:37 +08:00
Attack825
1b5ef53655 feat: fix tour bug about orgIsTourVisible settings (#3965) 2025-07-16 18:00:44 +08:00
Attack825
18d639cca2 feat: fix tour button (#3961) 2025-07-16 12:02:14 +08:00
DacongDA
3ac5aad648 feat: fix validate text error caused by password length check (#3964) 2025-07-16 10:10:13 +08:00
Robin Ye
2a53241128 feat: support 15 more currencies (#3963) 2025-07-16 01:07:25 +08:00
DacongDA
835273576b feat: add Lark OAuth provider (#3956) 2025-07-13 19:51:45 +08:00
raiki02
7fdc264ff6 feat: check if MFA is verified when required (#3954) 2025-07-12 15:20:44 +08:00
DacongDA
a120734bb1 feat: support links in email to reset password (#3939) 2025-07-12 00:18:56 +08:00
Vickko
edd0b30e08 feat: Supports smooth migration of password hash (#3940) 2025-07-11 19:57:55 +08:00
Attack825
2da597b26f feat: add support for per-account MFA validity period in org setting to reduce repeated prompts (#3917) 2025-07-11 00:24:33 +08:00
DacongDA
ef14c84edc feat: show the popover on the top when window's width too small and close popover when password options is empty (#3952) 2025-07-10 19:56:05 +08:00
Yang Luo
cb5c7667b5 feat: change Subscription's StartTime and EndTime to string 2025-07-10 14:11:40 +08:00
Yang Luo
920ed87f75 fix: refactor the code in CheckPassword() 2025-07-10 00:49:13 +08:00
raiki02
6598f0ccdf feat: use token's client ID instead in IntrospectToken() API (#3948) 2025-07-09 22:07:44 +08:00
Yang Luo
8e71e23d75 feat: improve error message for GetConfigInt64() 2025-07-09 00:32:00 +08:00
Yang Luo
146a369f80 feat: improve error handling in AutoSigninFilter 2025-07-08 23:47:14 +08:00
raiki02
9bbe5afb7c feat: use only one salt arg in CredManager.IsPasswordCorrect() (#3936) 2025-07-07 17:56:25 +08:00
DacongDA
b42391c6ce feat: move needUpdatePassword to response's Data3 field to avoid refresh token conflict (#3931) 2025-07-05 22:48:44 +08:00
Raiki
fb035a5353 feat: CredManager.GetHashedPassword() only contains one salt arg now (#3928) 2025-07-05 18:41:37 +08:00
Raiki
b1f68a60a4 feat: set createDatabase to false in TestDumpToFile() (#3924) 2025-07-03 22:50:23 +08:00
Robin Ye
201d704a31 feat: improve TikTok username generation logic (#3923) 2025-07-03 20:53:15 +08:00
Robin Ye
bf91ad6c97 feat: add Internet-Only captcha rule (#3919) 2025-07-03 02:39:06 +08:00
Yang Luo
3ccc0339c7 feat: improve CheckToEnableCaptcha() logic 2025-07-03 02:32:07 +08:00
DacongDA
1f2b0a3587 feat: add user's MFA items (#3921) 2025-07-02 23:05:07 +08:00
DacongDA
0b3feb0d5f feat: use Input.OTP to input totp code (#3922) 2025-07-02 18:22:59 +08:00
122 changed files with 1726 additions and 360 deletions

View File

@@ -66,7 +66,11 @@ func GetConfigBool(key string) bool {
func GetConfigInt64(key string) (int64, error) {
value := GetConfigString(key)
num, err := strconv.ParseInt(value, 10, 64)
return num, err
if err != nil {
return 0, fmt.Errorf("GetConfigInt64(%s) error, %s", key, err.Error())
}
return num, nil
}
func GetConfigDataSourceName() string {

View File

@@ -42,6 +42,7 @@ type Response struct {
Name string `json:"name"`
Data interface{} `json:"data"`
Data2 interface{} `json:"data2"`
Data3 interface{} `json:"data3"`
}
type Captcha struct {
@@ -285,8 +286,7 @@ func (c *ApiController) Signup() {
}
}
if application.HasPromptPage() && user.Type == "normal-user" {
// The prompt page needs the user to be signed in
if user.Type == "normal-user" {
c.SetSessionUsername(user.GetId())
}

View File

@@ -132,7 +132,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
resp = &Response{Status: "ok", Msg: "", Data: userId, Data3: user.NeedUpdatePassword}
} else if form.Type == ResponseTypeCode {
clientId := c.Input().Get("clientId")
responseType := c.Input().Get("responseType")
@@ -154,7 +154,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
}
resp = codeToResponse(code)
resp.Data2 = user.NeedUpdatePassword
resp.Data3 = user.NeedUpdatePassword
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
@@ -168,7 +168,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
resp.Data2 = user.NeedUpdatePassword
resp.Data3 = user.NeedUpdatePassword
}
} else if form.Type == ResponseTypeDevice {
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
@@ -195,14 +195,14 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
object.DeviceAuthMap.Store(authCacheCast.UserName, deviceAuthCacheDeviceCodeCast)
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
resp = &Response{Status: "ok", Msg: "", Data: userId, Data3: user.NeedUpdatePassword}
} else if form.Type == ResponseTypeSaml { // saml flow
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error(), nil)
return
}
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]interface{}{"redirectUrl": redirectUrl, "method": method, "needUpdatePassword": user.NeedUpdatePassword}}
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]interface{}{"redirectUrl": redirectUrl, "method": method}, Data3: user.NeedUpdatePassword}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
@@ -355,20 +355,27 @@ func isProxyProviderType(providerType string) bool {
func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
if object.IsNeedPromptMfa(organization, user) {
// The prompt page needs the user to be srigned in
// The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId())
c.ResponseOk(object.RequiredMfa)
return true
}
if user.IsMfaEnabled() {
currentTime := util.String2Time(util.GetCurrentTime())
mfaRememberDeadline := util.String2Time(user.MfaRememberDeadline)
if user.MfaRememberDeadline != "" && mfaRememberDeadline.After(currentTime) {
return false
}
c.setMfaUserSession(user.GetId())
mfaList := object.GetAllMfaProps(user, true)
mfaAllowList := []*object.MfaProps{}
mfaRememberInHours := organization.MfaRememberInHours
for _, prop := range mfaList {
if prop.MfaType == verificationType || !prop.Enabled {
continue
}
prop.MfaRememberInHours = mfaRememberInHours
mfaAllowList = append(mfaAllowList, prop)
}
if len(mfaAllowList) >= 1 {
@@ -555,8 +562,11 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application"))
return
}
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
var enableCaptcha bool
if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); err != nil {
if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username, clientIp); err != nil {
c.ResponseError(err.Error())
return
} else if enableCaptcha {
@@ -970,6 +980,28 @@ func (c *ApiController) Login() {
return
}
var application *object.Application
if authForm.ClientId == "" {
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
} else {
application, err = object.GetApplicationByClientId(authForm.ClientId)
}
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
var organization *object.Organization
organization, err = object.GetOrganization(util.GetId("admin", application.Organization))
if err != nil {
c.ResponseError(c.T(err.Error()))
}
if authForm.Passcode != "" {
if authForm.MfaType == c.GetSession("verificationCodeType") {
c.ResponseError("Invalid multi-factor authentication type")
@@ -996,6 +1028,17 @@ func (c *ApiController) Login() {
}
}
if authForm.EnableMfaRemember {
mfaRememberInSeconds := organization.MfaRememberInHours * 3600
currentTime := util.String2Time(util.GetCurrentTime())
duration := time.Duration(mfaRememberInSeconds) * time.Second
user.MfaRememberDeadline = util.Time2String(currentTime.Add(duration))
_, err = object.UpdateUser(user.GetId(), user, []string{"mfa_remember_deadline"}, user.IsAdmin)
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.SetSession("verificationCodeType", "")
} else if authForm.RecoveryCode != "" {
err = object.MfaRecover(user, authForm.RecoveryCode)
@@ -1008,22 +1051,6 @@ func (c *ApiController) Login() {
return
}
var application *object.Application
if authForm.ClientId == "" {
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
} else {
application, err = object.GetApplicationByClientId(authForm.ClientId)
}
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
c.setMfaUserSession("")
@@ -1222,27 +1249,26 @@ func (c *ApiController) GetQRCode() {
func (c *ApiController) GetCaptchaStatus() {
organization := c.Input().Get("organization")
userId := c.Input().Get("userId")
user, err := object.GetUserByFields(organization, userId)
applicationName := c.Input().Get("application")
application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName))
if err != nil {
c.ResponseError(err.Error())
return
}
captchaEnabled := false
if user != nil {
var failedSigninLimit int
failedSigninLimit, _, err = object.GetFailedSigninConfigByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
if user.SigninWrongTimes >= failedSigninLimit {
captchaEnabled = true
}
if application == nil {
c.ResponseError("application not found")
return
}
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
captchaEnabled, err := object.CheckToEnableCaptcha(application, organization, userId, clientIp)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(captchaEnabled)
return
}
// Callback

View File

@@ -58,6 +58,12 @@ func (c *ApiController) MfaSetupInitiate() {
return
}
organization, err := object.GetOrganizationByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
mfaProps, err := MfaUtil.Initiate(user.GetId())
if err != nil {
c.ResponseError(err.Error())
@@ -66,6 +72,7 @@ func (c *ApiController) MfaSetupInitiate() {
recoveryCode := uuid.NewString()
mfaProps.RecoveryCodes = []string{recoveryCode}
mfaProps.MfaRememberInHours = organization.MfaRememberInHours
resp := mfaProps
c.ResponseOk(resp)

View File

@@ -98,6 +98,10 @@ func (c *ApiController) GetOrganization() {
return
}
if organization != nil && organization.MfaRememberInHours == 0 {
organization.MfaRememberInHours = 12
}
c.ResponseOk(organization)
}

View File

@@ -140,6 +140,9 @@ func (c *ApiController) SendEmail() {
}
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
matchContent := object.ResetLinkReg.Find([]byte(content))
content = strings.Replace(content, string(matchContent), "", -1)
for _, receiver := range emailForm.Receivers {
err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender)
if err != nil {

View File

@@ -16,6 +16,7 @@ package controllers
import (
"encoding/json"
"fmt"
"time"
"github.com/beego/beego/utils/pagination"
@@ -460,7 +461,18 @@ func (c *ApiController) IntrospectToken() {
}
if token != nil {
application, err = object.GetApplication(fmt.Sprintf("%s/%s", token.Owner, token.Application))
if err != nil {
c.ResponseTokenError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), token.Application))
return
}
introspectionResponse.TokenType = token.TokenType
introspectionResponse.ClientId = application.ClientId
}
c.Data["json"] = introspectionResponse

View File

@@ -574,7 +574,7 @@ func (c *ApiController) SetPassword() {
targetUser.LastChangePasswordTime = util.GetCurrentTime()
if user.Ldap == "" {
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
_, err = object.UpdateUser(userId, targetUser, []string{"password", "password_salt", "need_update_password", "password_type", "last_change_password_time"}, false)
} else {
if isAdmin {
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())

View File

@@ -54,13 +54,6 @@ func (c *ApiController) ResponseError(error string, data ...interface{}) {
return
}
enableErrorMask := conf.GetConfigBool("enableErrorMask")
if enableErrorMask {
if strings.HasPrefix(error, "The user: ") && strings.HasSuffix(error, " doesn't exist") || strings.HasPrefix(error, "用户: ") && strings.HasSuffix(error, "不存在") {
error = c.T("check:password or code is incorrect")
}
}
resp := &Response{Status: "error", Msg: error}
c.ResponseJsonData(resp, data...)
}

View File

@@ -258,7 +258,7 @@ func (c *ApiController) SendVerificationCode() {
return
}
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, clientIp, vform.Dest)
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, clientIp, vform.Dest, vform.Method, c.Ctx.Request.Host, application.Name)
case object.VerifyTypePhone:
if vform.Method == LoginVerification || vform.Method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {

View File

@@ -23,7 +23,7 @@ func NewArgon2idCredManager() *Argon2idCredManager {
return cm
}
func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *Argon2idCredManager) GetHashedPassword(password string, salt string) string {
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
return ""
@@ -31,7 +31,7 @@ func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt strin
return hash
}
func (cm *Argon2idCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
func (cm *Argon2idCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
match, _ := argon2id.ComparePasswordAndHash(plainPwd, hashedPwd)
return match
}

View File

@@ -9,7 +9,7 @@ func NewBcryptCredManager() *BcryptCredManager {
return cm
}
func (cm *BcryptCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *BcryptCredManager) GetHashedPassword(password string, salt string) string {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return ""
@@ -17,7 +17,7 @@ func (cm *BcryptCredManager) GetHashedPassword(password string, userSalt string,
return string(bytes)
}
func (cm *BcryptCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
func (cm *BcryptCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(plainPwd))
return err == nil
}

View File

@@ -15,8 +15,8 @@
package cred
type CredManager interface {
GetHashedPassword(password string, userSalt string, organizationSalt string) string
IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool
GetHashedPassword(password string, salt string) string
IsPasswordCorrect(password string, passwordHash string, salt string) bool
}
func GetCredManager(passwordType string) CredManager {

View File

@@ -37,14 +37,21 @@ func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
return cm
}
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
res := getMd5HexDigest(password)
if userSalt != "" {
res = getMd5HexDigest(res + userSalt)
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, salt string) string {
if salt == "" {
return getMd5HexDigest(password)
}
return res
return getMd5HexDigest(getMd5HexDigest(password) + salt)
}
func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
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)
}

View File

@@ -28,13 +28,13 @@ func NewPbkdf2SaltCredManager() *Pbkdf2SaltCredManager {
return cm
}
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, salt string) string {
// https://www.keycloak.org/docs/latest/server_admin/index.html#password-database-compromised
decodedSalt, _ := base64.StdEncoding.DecodeString(userSalt)
decodedSalt, _ := base64.StdEncoding.DecodeString(salt)
res := pbkdf2.Key([]byte(password), decodedSalt, 27500, 64, sha256.New)
return base64.StdEncoding.EncodeToString(res)
}
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
}

View File

@@ -32,12 +32,8 @@ func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
return cm
}
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, salt string) string {
iterations := 260000
salt := userSalt
if salt == "" {
salt = organizationSalt
}
saltBytes := []byte(salt)
passwordBytes := []byte(password)
@@ -46,7 +42,7 @@ func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt st
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
}
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool {
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, _salt string) bool {
parts := strings.Split(passwordHash, "$")
if len(parts) != 4 {
return false

View File

@@ -21,10 +21,10 @@ func NewPlainCredManager() *PlainCredManager {
return cm
}
func (cm *PlainCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *PlainCredManager) GetHashedPassword(password string, salt string) string {
return password
}
func (cm *PlainCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
func (cm *PlainCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == plainPwd
}

View File

@@ -37,14 +37,21 @@ func NewSha256SaltCredManager() *Sha256SaltCredManager {
return cm
}
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
res := getSha256HexDigest(password)
if organizationSalt != "" {
res = getSha256HexDigest(res + organizationSalt)
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, salt string) string {
if salt == "" {
return getSha256HexDigest(password)
}
return res
return getSha256HexDigest(getSha256HexDigest(password) + salt)
}
func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
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)
}

View File

@@ -23,12 +23,12 @@ func TestGetSaltedPassword(t *testing.T) {
password := "123456"
salt := "123"
cm := NewSha256SaltCredManager()
fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, "", salt))
fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, salt))
}
func TestGetPassword(t *testing.T) {
password := "123456"
cm := NewSha256SaltCredManager()
// https://passwordsgenerator.net/sha256-hash-generator/
fmt.Printf("%s -> %s\n", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", cm.GetHashedPassword(password, "", ""))
fmt.Printf("%s -> %s\n", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", cm.GetHashedPassword(password, ""))
}

View File

@@ -37,14 +37,21 @@ func NewSha512SaltCredManager() *Sha512SaltCredManager {
return cm
}
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
res := getSha512HexDigest(password)
if organizationSalt != "" {
res = getSha512HexDigest(res + organizationSalt)
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, salt string) string {
if salt == "" {
return getSha512HexDigest(password)
}
return res
return getSha512HexDigest(getSha512HexDigest(password) + salt)
}
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
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)
}

View File

@@ -61,9 +61,10 @@ type AuthForm struct {
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
MfaType string `json:"mfaType"`
Passcode string `json:"passcode"`
RecoveryCode string `json:"recoveryCode"`
MfaType string `json:"mfaType"`
Passcode string `json:"passcode"`
RecoveryCode string `json:"recoveryCode"`
EnableMfaRemember bool `json:"enableMfaRemember"`
Plan string `json:"plan"`
Pricing string `json:"pricing"`

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
"password or code is incorrect": "heslo nebo kód je nesprávné",
"password or code is incorrect, you have %d remaining chances": "heslo nebo kód je nesprávné, máte %d zbývajících pokusů",
"password or code is incorrect, you have %s remaining chances": "heslo nebo kód je nesprávné, máte %s zbývajících pokusů",
"unsupported password type: %s": "nepodporovaný typ hesla: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Das Passwort oder der Code ist falsch. Du hast noch %d Versuche übrig",
"password or code is incorrect, you have %s remaining chances": "Das Passwort oder der Code ist falsch. Du hast noch %s Versuche übrig",
"unsupported password type: %s": "Nicht unterstützter Passworttyp: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Contraseña o código incorrecto, tienes %d intentos restantes",
"password or code is incorrect, you have %s remaining chances": "Contraseña o código incorrecto, tienes %s intentos restantes",
"unsupported password type: %s": "Tipo de contraseña no compatible: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبت‌نام با تلفن را ندارد",
"password or code is incorrect": "رمز عبور یا کد نادرست است",
"password or code is incorrect, you have %d remaining chances": "رمز عبور یا کد نادرست است، شما %d فرصت باقی‌مانده دارید",
"password or code is incorrect, you have %s remaining chances": "رمز عبور یا کد نادرست است، شما %s فرصت باقی‌مانده دارید",
"unsupported password type: %s": "نوع رمز عبور پشتیبانی نشده: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
"password or code is incorrect": "mot de passe ou code invalide",
"password or code is incorrect, you have %d remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %d chances",
"password or code is incorrect, you have %s remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %s chances",
"unsupported password type: %s": "Type de mot de passe non pris en charge : %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
"password or code is incorrect": "kata sandi atau kode salah",
"password or code is incorrect, you have %d remaining chances": "Sandi atau kode salah, Anda memiliki %d kesempatan tersisa",
"password or code is incorrect, you have %s remaining chances": "Sandi atau kode salah, Anda memiliki %s kesempatan tersisa",
"unsupported password type: %s": "jenis sandi tidak didukung: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "パスワードまたはコードが間違っています。あと%d回の試行機会があります",
"password or code is incorrect, you have %s remaining chances": "パスワードまたはコードが間違っています。あと%s回の試行機会があります",
"unsupported password type: %s": "サポートされていないパスワードタイプ:%s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "암호 또는 코드가 올바르지 않습니다. %d번의 기회가 남아 있습니다",
"password or code is incorrect, you have %s remaining chances": "암호 또는 코드가 올바르지 않습니다. %s번의 기회가 남아 있습니다",
"unsupported password type: %s": "지원되지 않는 암호 유형: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "senha ou código incorreto",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
"password or code is incorrect": "неправильный пароль или код",
"password or code is incorrect, you have %d remaining chances": "Неправильный пароль или код, у вас осталось %d попыток",
"password or code is incorrect, you have %s remaining chances": "Неправильный пароль или код, у вас осталось %s попыток",
"unsupported password type: %s": "неподдерживаемый тип пароля: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
"password or code is incorrect": "heslo alebo kód je nesprávne",
"password or code is incorrect, you have %d remaining chances": "heslo alebo kód je nesprávne, máte %d zostávajúcich pokusov",
"password or code is incorrect, you have %s remaining chances": "heslo alebo kód je nesprávne, máte %s zostávajúcich pokusov",
"unsupported password type: %s": "nepodporovaný typ hesla: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "şifre veya kod hatalı",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Mật khẩu hoặc mã không chính xác, bạn còn %d lần cơ hội",
"password or code is incorrect, you have %s remaining chances": "Mật khẩu hoặc mã không chính xác, bạn còn %s lần cơ hội",
"unsupported password type: %s": "Loại mật khẩu không được hỗ trợ: %s"
},
"enforcer": {

View File

@@ -85,7 +85,7 @@
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "您的密码已过期。请点击 \\\"忘记密码\\\" 以重置密码",
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
"password or code is incorrect": "密码错误",
"password or code is incorrect, you have %d remaining chances": "密码错误,您还有 %d 次尝试的机会",
"password or code is incorrect, you have %s remaining chances": "密码错误,您还有 %s 次尝试的机会",
"unsupported password type: %s": "不支持的密码类型: %s"
},
"enforcer": {

View File

@@ -19,14 +19,21 @@ import (
"fmt"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
)
var enableErrorMask = false
//go:embed locales/*/data.json
var f embed.FS
var langMap = make(map[string]map[string]map[string]string) // for example : langMap[en][account][Invalid information] = Invalid information
func init() {
enableErrorMask = conf.GetConfigBool("enableErrorMask")
}
func getI18nFilePath(category string, language string) string {
if category == "backend" {
return fmt.Sprintf("../i18n/locales/%s/data.json", language)
@@ -74,6 +81,15 @@ func applyData(data1 *I18nData, data2 *I18nData) {
}
func Translate(language string, errorText string) string {
modified := false
if enableErrorMask {
if errorText == "general:The user: %s doesn't exist" ||
errorText == "check:password or code is incorrect, you have %s remaining chances" {
modified = true
errorText = "check:password or code is incorrect"
}
}
tokens := strings.SplitN(errorText, ":", 2)
if !strings.Contains(errorText, ":") || len(tokens) != 2 {
return fmt.Sprintf("Translate error: the error text doesn't contain \":\", errorText = %s", errorText)
@@ -97,5 +113,9 @@ func Translate(language string, errorText string) string {
if res == "" {
res = tokens[1]
}
if modified {
res += "%.s"
}
return res
}

View File

@@ -190,7 +190,7 @@ func (idp *DouyinIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
userInfo := UserInfo{
Id: douyinUserInfo.Data.OpenId,
Username: douyinUserInfo.Data.Nickname,
Username: douyinUserInfo.Data.OpenId,
DisplayName: douyinUserInfo.Data.Nickname,
AvatarUrl: douyinUserInfo.Data.Avatar,
}

View File

@@ -27,16 +27,22 @@ import (
)
type LarkIdProvider struct {
Client *http.Client
Config *oauth2.Config
Client *http.Client
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{}
if useGlobalEndpoint {
idp.LarkDomain = "https://open.larksuite.com"
} else {
idp.LarkDomain = "https://open.feishu.cn"
}
config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config
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
func (idp *LarkIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
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{
@@ -162,6 +168,7 @@ type LarkUserInfo struct {
} `json:"data"`
}
// GetUserInfo use LarkAccessToken gotten before return LinkedInUserInf
// 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
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
}
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 {
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)
}
case "Lark":
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
case "GitLab":
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "ADFS":

View File

@@ -220,10 +220,15 @@ func checkSigninErrorTimes(user *User, lang string) error {
}
func CheckPassword(user *User, password string, lang string, options ...bool) error {
if password == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
}
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// check the login error times
if !enableCaptcha {
err := checkSigninErrorTimes(user, lang)
@@ -236,35 +241,41 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
if err != nil {
return err
}
if organization == nil {
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
}
if password == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
}
passwordType := user.PasswordType
if passwordType == "" {
passwordType = organization.PasswordType
}
credManager := cred.GetCredManager(passwordType)
if credManager != nil {
if organization.MasterPassword != "" {
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
return resetUserSigninErrorTimes(user)
}
}
if credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt, organization.PasswordSalt) {
credManager := cred.GetCredManager(passwordType)
if credManager == nil {
return fmt.Errorf(i18n.Translate(lang, "check:unsupported password type: %s"), passwordType)
}
if organization.MasterPassword != "" {
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, organization.PasswordSalt) {
return resetUserSigninErrorTimes(user)
}
return recordSigninErrorInfo(user, lang, enableCaptcha)
} else {
return fmt.Errorf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
}
if !credManager.IsPasswordCorrect(password, user.Password, organization.PasswordSalt) && !credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt) {
return recordSigninErrorInfo(user, lang, enableCaptcha)
}
isOutdated := passwordType != organization.PasswordType
if isOutdated {
user.Password = password
user.UpdateUserPassword(organization)
_, err = UpdateUser(user.GetId(), user, []string{"password", "password_type", "password_salt"}, true)
if err != nil {
return err
}
}
return resetUserSigninErrorTimes(user)
}
func CheckPasswordComplexityByOrg(organization *Organization, password string) string {
@@ -593,31 +604,41 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
return ""
}
func CheckToEnableCaptcha(application *Application, organization, username string) (bool, error) {
func CheckToEnableCaptcha(application *Application, organization, username string, clientIp string) (bool, error) {
if len(application.Providers) == 0 {
return false, nil
}
for _, providerItem := range application.Providers {
if providerItem.Provider == nil {
if providerItem.Provider == nil || providerItem.Provider.Category != "Captcha" {
continue
}
if providerItem.Provider.Category == "Captcha" {
if providerItem.Rule == "Dynamic" {
user, err := GetUserByFields(organization, username)
if providerItem.Rule == "Internet-Only" {
if util.IsInternetIp(clientIp) {
return true, nil
}
}
if providerItem.Rule == "Dynamic" {
user, err := GetUserByFields(organization, username)
if err != nil {
return false, err
}
if user != nil {
failedSigninLimit, _, err := GetFailedSigninConfigByUser(user)
if err != nil {
return false, err
}
failedSigninLimit := application.FailedSigninLimit
if failedSigninLimit == 0 {
failedSigninLimit = DefaultFailedSigninLimit
}
return user != nil && user.SigninWrongTimes >= failedSigninLimit, nil
return user.SigninWrongTimes >= failedSigninLimit, nil
}
return providerItem.Rule == "Always", nil
return false, nil
}
return providerItem.Rule == "Always", nil
}
return false, nil

View File

@@ -17,6 +17,7 @@ package object
import (
"fmt"
"regexp"
"strconv"
"time"
"github.com/casdoor/casdoor/i18n"
@@ -100,7 +101,7 @@ func recordSigninErrorInfo(user *User, lang string, options ...bool) error {
if leftChances == 0 && enableCaptcha {
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect"))
} else if leftChances >= 0 {
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances)
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %s remaining chances"), strconv.Itoa(leftChances))
}
// don't show the chance error message if the user has no chance left

View File

@@ -103,7 +103,7 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
func countCreatedBefore(dashboardMapItem DashboardMapItem, before time.Time) int64 {
count := dashboardMapItem.itemCount
for _, e := range dashboardMapItem.dashboardDateItems {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", e.CreatedTime)
createdTime, _ := time.Parse(time.RFC3339, e.CreatedTime)
if createdTime.Before(before) {
count++
}

View File

@@ -20,6 +20,7 @@ package object
import "testing"
func TestDumpToFile(t *testing.T) {
createDatabase = false
InitConfig()
err := DumpToFile("./init_data_dump.json")

View File

@@ -21,13 +21,14 @@ import (
)
type MfaProps struct {
Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"`
MfaType string `json:"mfaType" form:"mfaType"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
RecoveryCodes []string `json:"recoveryCodes,omitempty"`
Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"`
MfaType string `json:"mfaType" form:"mfaType"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
RecoveryCodes []string `json:"recoveryCodes,omitempty"`
MfaRememberInHours int `json:"mfaRememberInHours"`
}
type MfaInterface interface {

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", "form_post"},
GrantTypesSupported: []string{"password", "authorization_code"},
SubjectTypesSupported: []string{"public"},
IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},

View File

@@ -81,11 +81,12 @@ type Organization struct {
UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"`
IpRestriction string `json:"ipRestriction"`
NavItems []string `xorm:"varchar(1000)" json:"navItems"`
WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"`
NavItems []string `xorm:"mediumtext" json:"navItems"`
WidgetItems []string `xorm:"mediumtext" json:"widgetItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberInHours int `json:"mfaRememberInHours"`
AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
}
func GetOrganizationCount(owner, name, field, value string) (int64, error) {
@@ -222,7 +223,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
if organization.MasterPassword != "" && organization.MasterPassword != "***" {
credManager := cred.GetCredManager(organization.PasswordType)
if credManager != nil {
hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, "", organization.PasswordSalt)
hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, organization.PasswordSalt)
organization.MasterPassword = hashedPassword
}
}
@@ -536,7 +537,13 @@ func IsNeedPromptMfa(org *Organization, user *User) bool {
if org == nil || user == nil {
return false
}
for _, item := range org.MfaItems {
mfaItems := org.MfaItems
if len(user.MfaItems) > 0 {
mfaItems = user.MfaItems
}
for _, item := range mfaItems {
if item.Rule == "Required" {
if item.Name == EmailType && !user.MfaEmailEnabled {
return true

View File

@@ -49,17 +49,21 @@ func (plan *Plan) GetId() string {
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
}
func GetDuration(period string) (startTime time.Time, endTime time.Time) {
func getDuration(period string) (string, string, error) {
startTime := time.Now()
var endTime time.Time
if period == PeriodYearly {
startTime = time.Now()
endTime = startTime.AddDate(1, 0, 0)
} else if period == PeriodMonthly {
startTime = time.Now()
endTime = startTime.AddDate(0, 1, 0)
} else {
panic(fmt.Sprintf("invalid period: %s", period))
return "", "", fmt.Errorf("invalid period: %s", period)
}
return
startTimeString := startTime.Format(time.RFC3339)
endTimeString := endTime.Format(time.RFC3339)
return startTimeString, endTimeString, nil
}
func GetPlanCount(owner, field, value string) (int64, error) {

View File

@@ -206,11 +206,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if plan == nil {
return nil, nil, fmt.Errorf("the plan: %s does not exist", planName)
}
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
if err != nil {
return nil, nil, err
}
_, err = AddSubscription(sub)
if err != nil {
return nil, nil, err
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
}

View File

@@ -48,8 +48,8 @@ type Subscription struct {
Plan string `xorm:"varchar(100)" json:"plan"`
Payment string `xorm:"varchar(100)" json:"payment"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
StartTime string `xorm:"varchar(100)" json:"startTime"`
EndTime string `xorm:"varchar(100)" json:"endTime"`
Period string `xorm:"varchar(100)" json:"period"`
State SubscriptionState `xorm:"varchar(100)" json:"state"`
}
@@ -84,9 +84,19 @@ func (sub *Subscription) UpdateState() error {
}
if sub.State == SubStateActive || sub.State == SubStateUpcoming || sub.State == SubStateExpired {
if sub.EndTime.Before(time.Now()) {
startTime, err := time.Parse(time.RFC3339, sub.StartTime)
if err != nil {
return err
}
endTime, err := time.Parse(time.RFC3339, sub.EndTime)
if err != nil {
return err
}
if endTime.Before(time.Now()) {
sub.State = SubStateExpired
} else if sub.StartTime.After(time.Now()) {
} else if startTime.After(time.Now()) {
sub.State = SubStateUpcoming
} else {
sub.State = SubStateActive
@@ -103,10 +113,15 @@ func (sub *Subscription) UpdateState() error {
return nil
}
func NewSubscription(owner, userName, planName, paymentName, period string) *Subscription {
startTime, endTime := GetDuration(period)
func NewSubscription(owner, userName, planName, paymentName, period string) (*Subscription, error) {
startTime, endTime, err := getDuration(period)
if err != nil {
return nil, err
}
id := util.GenerateId()[:6]
return &Subscription{
res := &Subscription{
Owner: owner,
Name: "sub_" + id,
DisplayName: "New Subscription - " + id,
@@ -121,6 +136,7 @@ func NewSubscription(owner, userName, planName, paymentName, period string) *Sub
Period: period,
State: SubStatePending, // waiting for payment complete
}
return res, nil
}
func GetSubscriptionCount(owner, field, value string) (int64, error) {

View File

@@ -190,7 +190,7 @@ type User struct {
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
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"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
@@ -204,16 +204,18 @@ type User struct {
Roles []*Role `json:"roles"`
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"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberDeadline string `xorm:"varchar(100)" json:"mfaRememberDeadline"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
}
type Userinfo struct {
@@ -791,11 +793,11 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_items", "mfa_remember_deadline",
}
}
if isAdmin {
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance")
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "mfa_items")
}
columns = append(columns, "updated_time")

View File

@@ -42,8 +42,9 @@ func (user *User) UpdateUserHash() error {
func (user *User) UpdateUserPassword(organization *Organization) {
credManager := cred.GetCredManager(organization.PasswordType)
if credManager != nil {
hashedPassword := credManager.GetHashedPassword(user.Password, user.PasswordSalt, organization.PasswordSalt)
hashedPassword := credManager.GetHashedPassword(user.Password, organization.PasswordSalt)
user.Password = hashedPassword
user.PasswordType = organization.PasswordType
user.PasswordSalt = organization.PasswordSalt
}
}

View File

@@ -19,6 +19,8 @@ import (
"fmt"
"math"
"math/rand"
"net/url"
"regexp"
"strings"
"time"
@@ -33,6 +35,8 @@ type VerifyResult struct {
Msg string
}
var ResetLinkReg *regexp.Regexp
const (
VerificationSuccess = iota
wrongCodeError
@@ -45,6 +49,10 @@ const (
VerifyTypeEmail = "email"
)
func init() {
ResetLinkReg = regexp.MustCompile("(?s)<reset-link>(.*?)</reset-link>")
}
type VerificationRecord struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
@@ -81,7 +89,7 @@ func IsAllowSend(user *User, remoteAddr, recordType string) error {
return nil
}
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string, method string, host string, applicationName string) error {
sender := organization.DisplayName
title := provider.Title
@@ -93,6 +101,23 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := strings.Replace(provider.Content, "%s", code, 1)
if method == "forget" {
originFrontend, _ := getOriginFromHost(host)
query := url.Values{}
query.Add("code", code)
query.Add("username", user.Name)
query.Add("dest", util.GetMaskedEmail(dest))
forgetURL := originFrontend + "/forget/" + applicationName + "?" + query.Encode()
content = strings.Replace(content, "%link", forgetURL, -1)
content = strings.Replace(content, "<reset-link>", "", -1)
content = strings.Replace(content, "</reset-link>", "", -1)
} else {
matchContent := ResetLinkReg.Find([]byte(content))
content = strings.Replace(content, string(matchContent), "", -1)
}
userString := "Hi"
if user != nil {
userString = user.GetFriendlyName()

View File

@@ -66,6 +66,10 @@ func AutoSigninFilter(ctx *context.Context) {
responseError(ctx, err.Error())
return
}
if application == nil {
responseError(ctx, fmt.Sprintf("No application is found for userId: app/%s", token.Application))
return
}
setSessionUser(ctx, userId)
setSessionOidc(ctx, token.Scope, application.ClientId)

View File

@@ -185,17 +185,3 @@ func removePort(s string) string {
}
return ipStr
}
func isHostIntranet(s string) bool {
ipStr, _, err := net.SplitHostPort(s)
if err != nil {
ipStr = s
}
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

View File

@@ -83,7 +83,7 @@ func CorsFilter(ctx *context.Context) {
setCorsHeaders(ctx, origin)
} else if originHostname == host {
setCorsHeaders(ctx, origin)
} else if isHostIntranet(host) {
} else if util.IsHostIntranet(host) {
setCorsHeaders(ctx, origin)
} else {
ok, err := object.IsOriginAllowed(origin)

47
util/network.go Normal file
View File

@@ -0,0 +1,47 @@
// 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.
package util
import (
"net"
)
func IsInternetIp(ip string) bool {
ipStr, _, err := net.SplitHostPort(ip)
if err != nil {
ipStr = ip
}
parsedIP := net.ParseIP(ipStr)
if parsedIP == nil {
return false
}
return !parsedIP.IsPrivate() && !parsedIP.IsLoopback() && !parsedIP.IsMulticast() && !parsedIP.IsUnspecified()
}
func IsHostIntranet(ip string) bool {
ipStr, _, err := net.SplitHostPort(ip)
if err != nil {
ipStr = ip
}
parsedIP := net.ParseIP(ipStr)
if parsedIP == nil {
return false
}
return parsedIP.IsPrivate() || parsedIP.IsLoopback() || parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast()
}

View File

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

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

@@ -603,6 +603,16 @@ class OrganizationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:MFA remember time"), i18next.t("application:MFA remember time - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber style={{width: "150px"}} value={this.state.organization.mfaRememberInHours} min={1} step={1} precision={0} addonAfter="Hours" onChange={value => {
this.updateOrganizationField("mfaRememberInHours", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:MFA items"), i18next.t("general:MFA items - Tooltip"))} :

View File

@@ -25,6 +25,7 @@ import PopconfirmModal from "./common/modal/PopconfirmModal";
class OrganizationListPage extends BaseListPage {
newOrganization() {
const randomName = Setting.getRandomName();
const DefaultMfaRememberInHours = 12;
return {
owner: "admin", // this.props.account.organizationname,
name: `organization_${randomName}`,
@@ -48,6 +49,7 @@ class OrganizationListPage extends BaseListPage {
enableSoftDeletion: false,
isProfilePublic: true,
enableTour: true,
mfaRememberInHours: DefaultMfaRememberInHours,
accountItems: [
{name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},

View File

@@ -241,6 +241,21 @@ class PlanEditPage extends React.Component {
{id: "HKD", name: "HKD"},
{id: "SGD", name: "SGD"},
{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>)
}
</Select>

View File

@@ -141,6 +141,36 @@ class ProductBuyPage extends React.Component {
return "S$";
} else if (product?.currency === "BRL") {
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 {
return "(Unknown currency)";
}

View File

@@ -218,6 +218,21 @@ class ProductEditPage extends React.Component {
{id: "HKD", name: "HKD"},
{id: "SGD", name: "SGD"},
{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>)
}
</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"}} >
<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 span={1} >
<Switch disabled={!this.state.provider.clientId} checked={this.state.provider.disableSsl} onChange={checked => {
@@ -1227,7 +1229,7 @@ class ProviderEditPage extends React.Component {
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes.")} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes. <reset-link>Or click %link to reset</reset-link>")} >
{i18next.t("provider:Reset to Default Text")}
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => this.updateProviderField("content", Setting.getDefaultHtmlEmailContent())} >

View File

@@ -696,18 +696,27 @@ export const MfaRulePrompted = "Prompted";
export const MfaRuleOptional = "Optional";
export function isRequiredEnableMfa(user, organization) {
if (!user || !organization || !organization.mfaItems) {
if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
return false;
}
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
}
export function getMfaItemsByRules(user, organization, mfaRules = []) {
if (!user || !organization || !organization.mfaItems) {
if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
return [];
}
return organization.mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
let mfaItems = organization.mfaItems;
if (user.mfaItems && user.mfaItems.length !== 0) {
mfaItems = user.mfaItems;
}
if (mfaItems === null) {
return [];
}
return mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
.filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
}
@@ -1507,6 +1516,54 @@ export function getCurrencySymbol(currency) {
return "$";
} else if (currency === "CNY" || currency === "cny") {
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 {
return currency;
}
@@ -1571,6 +1628,11 @@ export function getDefaultHtmlEmailContent() {
<div class="code">
%s
</div>
<reset-link>
<div class="link">
Or click this <a href="%link">link</a> to reset
</div>
</reset-link>
<p>Thanks</p>
<p>Casbin Team</p>
<hr>
@@ -1605,6 +1667,36 @@ export function getCurrencyText(product) {
return i18next.t("currency:SGD");
} else if (product?.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 {
return "(Unknown currency)";
}

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

@@ -42,6 +42,7 @@ import * as MfaBackend from "./backend/MfaBackend";
import AccountAvatar from "./account/AccountAvatar";
import FaceIdTable from "./table/FaceIdTable";
import MfaAccountTable from "./table/MfaAccountTable";
import MfaTable from "./table/MfaTable";
const {Option} = Select;
@@ -926,6 +927,19 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "MFA items") {
return (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:MFA items"), i18next.t("general:MFA items - Tooltip"))} :
</Col>
<Col span={22} >
<MfaTable
title={i18next.t("general:MFA items")}
table={this.state.user.mfaItems ?? []}
onUpdateTable={(value) => {this.updateUserField("mfaItems", value);}}
/>
</Col>
</Row>);
} else if (accountItem.name === "Multi-factor authentication") {
return (
!this.isSelfOrAdmin() ? null : (

View File

@@ -163,7 +163,7 @@ export function getWechatQRCode(providerId) {
}
export function getCaptchaStatus(values) {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}`, {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}&application=${values["application"]}`, {
method: "GET",
credentials: "include",
headers: {

View File

@@ -35,6 +35,30 @@ class AuthCallback extends React.Component {
};
}
submitFormPost(redirectUri, code, state) {
const form = document.createElement("form");
form.method = "post";
form.action = redirectUri;
const codeInput = document.createElement("input");
codeInput.type = "hidden";
codeInput.name = "code";
codeInput.value = code;
form.appendChild(codeInput);
if (state) {
const stateInput = document.createElement("input");
stateInput.type = "hidden";
stateInput.name = "state";
stateInput.value = state;
form.appendChild(stateInput);
}
document.body.appendChild(form);
form.submit();
setTimeout(() => form.remove(), 1000);
}
getInnerParams() {
// For example, for Casbin-OA, realRedirectUri = "http://localhost:9000/login"
// realRedirectUrl = "http://localhost:9000"
@@ -158,6 +182,7 @@ class AuthCallback extends React.Component {
// OAuth
const oAuthParams = Util.getOAuthGetParameters(innerParams);
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
const responseMode = oAuthParams?.responseMode || "query"; // Default to "query" if not specified
const signinUrl = localStorage.getItem("signinUrl");
AuthBackend.login(body, oAuthParams)
@@ -166,7 +191,7 @@ class AuthCallback extends React.Component {
const responseType = this.getResponseType();
const handleLogin = (res) => {
if (responseType === "login") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
@@ -176,16 +201,21 @@ class AuthCallback extends React.Component {
const link = Setting.getFromLink();
Setting.goToLink(link);
} else if (responseType === "code") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
if (responseMode === "form_post") {
this.submitFormPost(oAuthParams?.redirectUri, res.data, oAuthParams?.state);
} else {
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
}
// Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
@@ -207,7 +237,7 @@ class AuthCallback extends React.Component {
relayState: oAuthParams.relayState,
});
} else {
if (res.data2.needUpdatePassword) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;

View File

@@ -31,18 +31,21 @@ const {Option} = Select;
class ForgetPage extends React.Component {
constructor(props) {
super(props);
const queryParams = new URLSearchParams(location.search);
this.state = {
classes: props,
applicationName: props.applicationName ?? props.match.params?.applicationName,
msg: null,
name: props.account ? props.account.name : "",
name: props.account ? props.account.name : queryParams.get("username"),
username: props.account ? props.account.name : "",
phone: "",
email: "",
dest: "",
isVerifyTypeFixed: false,
verifyType: "", // "email", "phone"
current: 0,
current: queryParams.get("code") ? 2 : 0,
code: queryParams.get("code"),
queryParams: queryParams,
};
this.form = React.createRef();
}
@@ -148,9 +151,26 @@ class ForgetPage extends React.Component {
}
}
onFinish(values) {
async onFinish(values) {
values.username = this.state.name;
values.userOwner = this.getApplicationObj()?.organizationObj.name;
if (this.state.queryParams.get("code")) {
const res = await UserBackend.verifyCode({
application: this.getApplicationObj().name,
organization: values.userOwner,
username: this.state.queryParams.get("dest"),
name: this.state.name,
code: this.state.code,
type: "login",
});
if (res.status !== "ok") {
Setting.showMessage("error", res.msg);
return;
}
}
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword, this.state.code).then(res => {
if (res.status === "ok") {
const linkInStorage = sessionStorage.getItem("signinUrl");
@@ -385,7 +405,7 @@ class ForgetPage extends React.Component {
},
]}
/>
<Popover placement="right" content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Popover placement={window.innerWidth >= 960 ? "right" : "top"} content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Form.Item
name="newPassword"
hidden={this.state.current !== 2}
@@ -415,7 +435,7 @@ class ForgetPage extends React.Component {
}}
onFocus={() => {
this.setState({
passwordPopoverOpen: true,
passwordPopoverOpen: application.organizationObj.passwordOptions?.length > 0,
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("newPassword") ?? ""),
});
}}

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"));
@@ -134,6 +135,8 @@ class LoginPage extends React.Component {
return CaptchaRule.Always;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) {
return CaptchaRule.Dynamic;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Internet-Only")) {
return CaptchaRule.InternetOnly;
} else {
return CaptchaRule.Never;
}
@@ -344,7 +347,7 @@ class LoginPage extends React.Component {
return;
}
if (resp.data2) {
if (resp.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLinkSoft(ths, `/forget/${application.name}`);
return;
@@ -434,15 +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;
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);
@@ -491,9 +505,9 @@ class LoginPage extends React.Component {
const responseType = values["type"];
if (responseType === "login") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
}
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
this.props.onLoginSuccess();
@@ -505,9 +519,9 @@ class LoginPage extends React.Component {
userCodeStatus: "success",
});
} else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
}
const amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
const accessToken = res.data;
@@ -517,9 +531,9 @@ class LoginPage extends React.Component {
this.props.onLoginSuccess(window.location.href);
return;
}
if (res.data2.needUpdatePassword) {
if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
}
if (res.data2.method === "POST") {
this.setState({
@@ -769,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>
);
@@ -813,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}} />
@@ -872,13 +888,17 @@ 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"
initialValues={{
organization: application.organization,
application: application.name,
autoSignin: true,
autoSignin: !application?.signinItems.map(signinItem => signinItem.name === "Forgot password?" && signinItem.rule === "Auto sign in - False")?.includes(true),
username: Conf.ShowGithubCorner ? "admin" : "",
password: Conf.ShowGithubCorner ? "123" : "",
}}
@@ -954,21 +974,41 @@ class LoginPage extends React.Component {
});
}
renderCaptchaModal(application) {
renderCaptchaModal(application, noModal) {
if (this.getCaptchaRule(this.getApplicationObj()) === CaptchaRule.Never) {
return null;
}
const captchaProviderItems = this.getCaptchaProviderItems(application);
const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always");
const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic");
const provider = alwaysProviderItems.length > 0
? alwaysProviderItems[0].provider
: dynamicProviderItems[0].provider;
const internetOnlyProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Internet-Only");
// Select provider based on the active captcha rule, not fixed priority
const captchaRule = this.getCaptchaRule(this.getApplicationObj());
let provider = null;
if (captchaRule === CaptchaRule.Always && alwaysProviderItems.length > 0) {
provider = alwaysProviderItems[0].provider;
} else if (captchaRule === CaptchaRule.Dynamic && dynamicProviderItems.length > 0) {
provider = dynamicProviderItems[0].provider;
} else if (captchaRule === CaptchaRule.InternetOnly && internetOnlyProviderItems.length > 0) {
provider = internetOnlyProviderItems[0].provider;
}
if (!provider) {
return null;
}
return <CaptchaModal
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;
@@ -1021,6 +1061,10 @@ class LoginPage extends React.Component {
return null;
}
if (this.props.requiredEnableMfa) {
return null;
}
if (this.state.userCode && this.state.userCodeStatus === "success") {
return null;
}
@@ -1181,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) => {
@@ -1202,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>

View File

@@ -68,6 +68,7 @@ const authInfo = {
Lark: {
// scope: "email",
endpoint: "https://open.feishu.cn/open-apis/authen/v1/index",
endpoint2: "https://accounts.larksuite.com/open-apis/authen/v1/authorize",
},
GitLab: {
scope: "read_user+profile",
@@ -406,6 +407,8 @@ export function getAuthUrl(application, provider, method, code) {
if (provider.domain) {
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"
@@ -460,6 +463,9 @@ export function getAuthUrl(application, provider, method, code) {
return `https://error:not-supported-provider-sub-type:${provider.subType}`;
}
} else if (provider.type === "Lark") {
if (provider.disableSsl) {
redirectUri = encodeURIComponent(redirectUri);
}
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`;
} 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`;

View File

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

View File

@@ -607,7 +607,7 @@ class SignupPage extends React.Component {
}
} else if (signupItem.name === "Password") {
return (
<Popover placement="right" content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Popover placement={window.innerWidth >= 960 ? "right" : "top"} content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Form.Item
name="password"
className="signup-password"
@@ -635,7 +635,7 @@ class SignupPage extends React.Component {
}}
onFocus={() => {
this.setState({
passwordPopoverOpen: true,
passwordPopoverOpen: application.organizationObj.passwordOptions?.length > 0,
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("password") ?? ""),
});
}}

View File

@@ -141,6 +141,7 @@ export function getOAuthGetParameters(params) {
const nonce = getRefinedValue(queries.get("nonce"));
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
const responseMode = getRefinedValue(queries.get("response_mode"));
const samlRequest = getRefinedValue(lowercaseQueries["samlRequest".toLowerCase()]);
const relayState = getRefinedValue(lowercaseQueries["RelayState".toLowerCase()]);
const noRedirect = getRefinedValue(lowercaseQueries["noRedirect".toLowerCase()]);
@@ -159,6 +160,7 @@ export function getOAuthGetParameters(params) {
nonce: nonce,
challengeMethod: challengeMethod,
codeChallenge: codeChallenge,
responseMode: responseMode,
samlRequest: samlRequest,
relayState: relayState,
noRedirect: noRedirect,

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

@@ -31,9 +31,9 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
const [mfaType, setMfaType] = useState(mfaProps.mfaType);
const [recoveryCode, setRecoveryCode] = useState("");
const verify = ({passcode}) => {
const verify = ({passcode, enableMfaRemember}) => {
setLoading(true);
const values = {...formValues, passcode};
const values = {...formValues, passcode, enableMfaRemember};
values["mfaType"] = mfaProps.mfaType;
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
loginFunction(values, authParams).then((res) => {

View File

@@ -1,5 +1,5 @@
import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input, Space} from "antd";
import {Button, Checkbox, Form, Input, Space} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
@@ -12,6 +12,13 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
const [dest, setDest] = React.useState("");
const [form] = Form.useForm();
const handleFinish = (values) => {
onFinish({
passcode: values.passcode,
enableMfaRemember: values.enableMfaRemember,
});
};
useEffect(() => {
if (method === mfaAuth) {
setDest(mfaProps.secret);
@@ -51,9 +58,10 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
onFinish={handleFinish}
initialValues={{
countryCode: mfaProps.countryCode,
enableMfaRemember: false,
}}
>
{isShowText() ?
@@ -109,6 +117,14 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
application={application}
/>
</Form.Item>
<Form.Item
name="enableMfaRemember"
valuePropName="checked"
>
<Checkbox>
{i18next.t("mfa:Remember this account for {hour} hours").replace("{hour}", mfaProps?.mfaRememberInHours)}
</Checkbox>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}

View File

@@ -1,5 +1,5 @@
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {Button, Col, Form, Input, QRCode, Space} from "antd";
import {CopyOutlined} from "@ant-design/icons";
import {Button, Checkbox, Col, Form, Input, QRCode, Space} from "antd";
import copy from "copy-to-clipboard";
import i18next from "i18next";
import React from "react";
@@ -8,6 +8,13 @@ import * as Setting from "../../Setting";
export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm();
const handleFinish = (values) => {
onFinish({
passcode: values.passcode,
enableMfaRemember: values.enableMfaRemember,
});
};
const renderSecret = () => {
if (!mfaProps.secret) {
return null;
@@ -40,20 +47,31 @@ export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
onFinish={handleFinish}
initialValues={{
enableMfaRemember: false,
}}
>
{renderSecret()}
<Form.Item
name="passcode"
rules={[{required: true, message: "Please input your passcode"}]}
>
<Input
<Input.OTP
style={{marginTop: 24}}
prefix={<UserOutlined />}
placeholder={i18next.t("mfa:Passcode")}
autoComplete="off"
onChange={() => {
form.submit();
}}
/>
</Form.Item>
<Form.Item
name="enableMfaRemember"
valuePropName="checked"
>
<Checkbox>
{i18next.t("mfa:Remember this account for {hour} hours").replace("{hour}", mfaProps?.mfaRememberInHours)}
</Checkbox>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}

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

@@ -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,32 +186,38 @@ 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 = {
Always: "Always",
Never: "Never",
Dynamic: "Dynamic",
InternetOnly: "Internet-Only",
};

View File

@@ -132,18 +132,18 @@ export const PasswordModal = (props) => {
</Row>
) : null}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Popover placement="right" content={passwordPopover} open={passwordPopoverOpen}>
<Popover placement={window.innerWidth >= 960 ? "right" : "top"} content={passwordPopover} open={passwordPopoverOpen}>
<Input.Password
addonBefore={i18next.t("user:New Password")}
placeholder={i18next.t("user:input password")}
onChange={(e) => {
handleNewPassword(e.target.value);
setPasswordPopoverOpen(true);
setPasswordPopoverOpen(passwordOptions?.length > 0);
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, e.target.value));
}}
onFocus={() => {
setPasswordPopoverOpen(true);
setPasswordPopoverOpen(passwordOptions?.length > 0);
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, newPassword));
}}
onBlur={() => {

View File

@@ -75,12 +75,16 @@
"Header HTML - Edit": "Header HTML - Edit",
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"Inline": "Inline",
"Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code",
"Left": "Left",
"Logged in successfully": "Logged in successfully",
"Logged out successfully": "Logged out successfully",
"MFA remember time": "MFA remember time",
"MFA remember time - Tooltip": "MFA remember time - Tooltip",
"Multiple Choices": "Multiple Choices",
"New Application": "New Application",
"No verification": "No verification",
@@ -92,6 +96,7 @@
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file",
"Pop up": "Pop up",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Redirect URL",
@@ -174,12 +179,27 @@
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD",
"USD": "USD"
"THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
},
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
@@ -277,6 +297,7 @@
"Failed to save": "Failed to save",
"Failed to sync": "Failed to sync",
"Failed to verify": "Failed to verify",
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
"First name": "First name",
@@ -426,6 +447,7 @@
"Tokens": "Tokens",
"Tour": "Tour",
"Transactions": "Transactions",
"True": "True",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
@@ -550,6 +572,7 @@
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
"Redirecting, please wait.": "Redirecting, please wait.",
"Refresh": "Refresh",
"Sign In": "Sign In",
"Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@@ -563,6 +586,7 @@
"The input is not valid phone number!": "The input is not valid phone number!",
"To access": "To access",
"Verification code": "Verification code",
"WeChat": "WeChat",
"WebAuthn": "WebAuthn",
"sign up now": "sign up now",
"username, Email or phone": "username, Email or phone"
@@ -579,13 +603,13 @@
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please confirm the information below": "Please confirm the information below",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Remember this account for {hour} hours": "Remember this account for {hour} hours",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
@@ -970,6 +994,8 @@
"Use WeChat Media Platform in PC - Tooltip": "Whether to allow scanning QR code to login",
"Use WeChat Media Platform to login": "Use WeChat Media Platform to login",
"Use WeChat Open Platform to login": "Use WeChat Open Platform to login",
"Use global endpoint": "Use global endpoint",
"Use global endpoint - Tooltip": "Use global endpoint - Tooltip",
"Use id as name": "Use id as name",
"Use id as name - Tooltip": "Use id as user's name",
"User flow": "User flow",

View File

@@ -75,12 +75,16 @@
"Header HTML - Edit": "Upravit HTML hlavičky",
"Header HTML - Tooltip": "Přizpůsobit hlavičku vstupní stránky vaší aplikace",
"Incremental": "Inkrementální",
"Inline": "Inline",
"Input": "Vstup",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Kód pozvánky",
"Left": "Vlevo",
"Logged in successfully": "Úspěšně přihlášen",
"Logged out successfully": "Úspěšně odhlášen",
"MFA remember time": "MFA remember time",
"MFA remember time - Tooltip": "MFA remember time - Tooltip",
"Multiple Choices": "Multiple Choices",
"New Application": "Nová aplikace",
"No verification": "Bez ověření",
@@ -92,6 +96,7 @@
"Please input your application!": "Zadejte svou aplikaci!",
"Please input your organization!": "Zadejte svou organizaci!",
"Please select a HTML file": "Vyberte HTML soubor",
"Pop up": "Pop up",
"Random": "Náhodný",
"Real name": "Skutečné jméno",
"Redirect URL": "Přesměrovací URL",
@@ -174,12 +179,27 @@
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD",
"USD": "USD"
"THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
},
"enforcer": {
"Edit Enforcer": "Upravit Enforcer",
@@ -277,6 +297,7 @@
"Failed to save": "Nepodařilo se uložit",
"Failed to sync": "Nepodařilo se synchronizovat",
"Failed to verify": "Nepodařilo se ověřit",
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "URL ikony favicon použité na všech stránkách Casdoor organizace",
"First name": "Křestní jméno",
@@ -426,6 +447,7 @@
"Tokens": "Tokeny",
"Tour": "Tour",
"Transactions": "Transakce",
"True": "True",
"Type": "Typ",
"Type - Tooltip": "Typ - Tooltip",
"URL": "URL",
@@ -550,6 +572,7 @@
"Please select an organization to sign in": "Vyberte organizaci pro přihlášení",
"Please type an organization to sign in": "Zadejte organizaci pro přihlášení",
"Redirecting, please wait.": "Přesměrování, prosím čekejte.",
"Refresh": "Refresh",
"Sign In": "Přihlásit se",
"Sign in with Face ID": "Přihlásit se pomocí Face ID",
"Sign in with WebAuthn": "Přihlásit se pomocí WebAuthn",
@@ -563,6 +586,7 @@
"The input is not valid phone number!": "Zadaný údaj není platné telefonní číslo!",
"To access": "Pro přístup",
"Verification code": "Ověřovací kód",
"WeChat": "WeChat",
"WebAuthn": "WebAuthn",
"sign up now": "zaregistrujte se nyní",
"username, Email or phone": "uživatelské jméno, Email nebo telefon"
@@ -579,13 +603,13 @@
"Multi-factor recover": "Obnovení dvoufaktorového ověřování",
"Multi-factor recover description": "Popis obnovení dvoufaktorového ověřování",
"Or copy the secret to your Authenticator App": "Nebo zkopírujte tajný kód do své aplikace Authenticator",
"Passcode": "Přístupový kód",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Nejprve prosím spojte svůj email, systém automaticky použije tento email pro dvoufaktorové ověřování",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Nejprve prosím spojte svůj telefon, systém automaticky použije tento telefon pro dvoufaktorové ověřování",
"Please confirm the information below": "Potvrďte prosím níže uvedené informace",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Uložte si tento obnovovací kód. Pokud vaše zařízení nemůže poskytnout ověřovací kód, můžete resetovat dvoufaktorové ověřování pomocí tohoto kódu",
"Protect your account with Multi-factor authentication": "Chraňte svůj účet pomocí dvoufaktorového ověřování",
"Recovery code": "Obnovovací kód",
"Remember this account for {hour} hours": "Remember this account for {hour} hours",
"Scan the QR code with your Authenticator App": "Naskenujte QR kód pomocí aplikace Authenticator",
"Set preferred": "Nastavit jako preferované",
"Setup": "Nastavení",
@@ -970,6 +994,8 @@
"Use WeChat Media Platform in PC - Tooltip": "Povolit skenování QR kódu WeChat Media Platform pro přihlášení",
"Use WeChat Media Platform to login": "Použít WeChat Media Platform pro přihlášení",
"Use WeChat Open Platform to login": "Použít WeChat Open Platform pro přihlášení",
"Use global endpoint": "Use global endpoint",
"Use global endpoint - Tooltip": "Use global endpoint - Tooltip",
"Use id as name": "Use id as name",
"Use id as name - Tooltip": "Use id as user's name",
"User flow": "Tok uživatele",

View File

@@ -75,12 +75,16 @@
"Header HTML - Edit": "Header HTML - Edit",
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"Inline": "Inline",
"Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code",
"Left": "Links",
"Logged in successfully": "Erfolgreich eingeloggt",
"Logged out successfully": "Erfolgreich ausgeloggt",
"MFA remember time": "MFA remember time",
"MFA remember time - Tooltip": "MFA remember time - Tooltip",
"Multiple Choices": "Multiple Choices",
"New Application": "Neue Anwendung",
"No verification": "No verification",
@@ -92,6 +96,7 @@
"Please input your application!": "Bitte geben Sie Ihre Anwendung ein!",
"Please input your organization!": "Bitte geben Sie Ihre Organisation ein!",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei aus",
"Pop up": "Pop up",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Weiterleitungs-URL",
@@ -174,12 +179,27 @@
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD",
"USD": "USD"
"THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
},
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
@@ -277,6 +297,7 @@
"Failed to save": "Konnte nicht gespeichert werden",
"Failed to sync": "Failed to sync",
"Failed to verify": "Failed to verify",
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon-URL, die auf allen Casdoor-Seiten der Organisation verwendet wird",
"First name": "Vorname",
@@ -426,6 +447,7 @@
"Tokens": "Token",
"Tour": "Tour",
"Transactions": "Transactions",
"True": "True",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
@@ -550,6 +572,7 @@
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
"Redirecting, please wait.": "Umleitung, bitte warten.",
"Refresh": "Refresh",
"Sign In": "Anmelden",
"Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Melden Sie sich mit WebAuthn an",
@@ -563,6 +586,7 @@
"The input is not valid phone number!": "The input is not valid phone number!",
"To access": "Zum Zugriff",
"Verification code": "Verifizierungscode",
"WeChat": "WeChat",
"WebAuthn": "WebAuthn",
"sign up now": "Melde dich jetzt an",
"username, Email or phone": "Benutzername, E-Mail oder Telefon"
@@ -579,13 +603,13 @@
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please confirm the information below": "Please confirm the information below",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Remember this account for {hour} hours": "Remember this account for {hour} hours",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
@@ -970,6 +994,8 @@
"Use WeChat Media Platform in PC - Tooltip": "Whether to allow scanning WeChat Media Platform QR code to login",
"Use WeChat Media Platform to login": "Use WeChat Media Platform to login",
"Use WeChat Open Platform to login": "Use WeChat Open Platform to login",
"Use global endpoint": "Use global endpoint",
"Use global endpoint - Tooltip": "Use global endpoint - Tooltip",
"Use id as name": "Use id as name",
"Use id as name - Tooltip": "Use id as user's name",
"User flow": "User flow",

View File

@@ -75,12 +75,16 @@
"Header HTML - Edit": "Header HTML - Edit",
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"Inline": "Inline",
"Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code",
"Left": "Left",
"Logged in successfully": "Logged in successfully",
"Logged out successfully": "Logged out successfully",
"MFA remember time": "MFA remember time",
"MFA remember time - Tooltip": "Configures the duration that a account is remembered as trusted after a successful MFA login",
"Multiple Choices": "Multiple Choices",
"New Application": "New Application",
"No verification": "No verification",
@@ -92,6 +96,7 @@
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file",
"Pop up": "Pop up",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Redirect URL",
@@ -174,12 +179,27 @@
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD",
"USD": "USD"
"THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
},
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
@@ -277,6 +297,7 @@
"Failed to save": "Failed to save",
"Failed to sync": "Failed to sync",
"Failed to verify": "Failed to verify",
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
"First name": "First name",
@@ -426,6 +447,7 @@
"Tokens": "Tokens",
"Tour": "Tour",
"Transactions": "Transactions",
"True": "True",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
@@ -550,6 +572,7 @@
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
"Redirecting, please wait.": "Redirecting, please wait.",
"Refresh": "Refresh",
"Sign In": "Sign In",
"Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@@ -563,6 +586,7 @@
"The input is not valid phone number!": "The input is not valid phone number!",
"To access": "To access",
"Verification code": "Verification code",
"WeChat": "WeChat",
"WebAuthn": "WebAuthn",
"sign up now": "sign up now",
"username, Email or phone": "username, Email or phone"
@@ -579,13 +603,13 @@
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please confirm the information below": "Please confirm the information below",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Remember this account for {hour} hours": "Remember this account for {hour} hours",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
@@ -970,6 +994,8 @@
"Use WeChat Media Platform in PC - Tooltip": "Whether to allow scanning WeChat Media Platform QR code to login",
"Use WeChat Media Platform to login": "Use WeChat Media Platform to login",
"Use WeChat Open Platform to login": "Use WeChat Open Platform to login",
"Use global endpoint": "Use global endpoint",
"Use global endpoint - Tooltip": "Use global endpoint - Tooltip",
"Use id as name": "Use id as name",
"Use id as name - Tooltip": "Use id as user's name",
"User flow": "User flow",

View File

@@ -75,12 +75,16 @@
"Header HTML - Edit": "Header HTML - Edit",
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"Inline": "Inline",
"Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code",
"Left": "Izquierda",
"Logged in successfully": "Acceso satisfactorio",
"Logged out successfully": "Cerró sesión exitosamente",
"MFA remember time": "MFA remember time",
"MFA remember time - Tooltip": "MFA remember time - Tooltip",
"Multiple Choices": "Multiple Choices",
"New Application": "Nueva aplicación",
"No verification": "No verification",
@@ -92,6 +96,7 @@
"Please input your application!": "¡Por favor, ingrese su solicitud!",
"Please input your organization!": "¡Por favor, ingrese su organización!",
"Please select a HTML file": "Por favor, seleccione un archivo HTML",
"Pop up": "Pop up",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Redireccionar URL",
@@ -174,12 +179,27 @@
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD",
"USD": "USD"
"THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
},
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
@@ -277,6 +297,7 @@
"Failed to save": "No se pudo guardar",
"Failed to sync": "Failed to sync",
"Failed to verify": "Failed to verify",
"False": "False",
"Favicon": "Favicon (ícono de favoritos)",
"Favicon - Tooltip": "URL del icono Favicon utilizado en todas las páginas de Casdoor de la organización",
"First name": "Nombre de pila",
@@ -426,6 +447,7 @@
"Tokens": "Tokens",
"Tour": "Tour",
"Transactions": "Transactions",
"True": "True",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "Dirección URL",
@@ -550,6 +572,7 @@
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
"Redirecting, please wait.": "Redirigiendo, por favor espera.",
"Refresh": "Refresh",
"Sign In": "Iniciar sesión",
"Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Iniciar sesión con WebAuthn",
@@ -563,6 +586,7 @@
"The input is not valid phone number!": "The input is not valid phone number!",
"To access": "para acceder",
"Verification code": "Código de verificación",
"WeChat": "WeChat",
"WebAuthn": "WebAuthn (Autenticación Web)",
"sign up now": "Regístrate ahora",
"username, Email or phone": "Nombre de usuario, correo electrónico o teléfono"
@@ -579,13 +603,13 @@
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please confirm the information below": "Please confirm the information below",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Remember this account for {hour} hours": "Remember this account for {hour} hours",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
@@ -970,6 +994,8 @@
"Use WeChat Media Platform in PC - Tooltip": "Whether to allow scanning WeChat Media Platform QR code to login",
"Use WeChat Media Platform to login": "Use WeChat Media Platform to login",
"Use WeChat Open Platform to login": "Use WeChat Open Platform to login",
"Use global endpoint": "Use global endpoint",
"Use global endpoint - Tooltip": "Use global endpoint - Tooltip",
"Use id as name": "Use id as name",
"Use id as name - Tooltip": "Use id as user's name",
"User flow": "User flow",

View File

@@ -75,12 +75,16 @@
"Header HTML - Edit": "ویرایش HTML سربرگ",
"Header HTML - Tooltip": "کد head صفحه ورود برنامه خود را سفارشی کنید",
"Incremental": "افزایشی",
"Inline": "Inline",
"Input": "ورودی",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "کد دعوت",
"Left": "چپ",
"Logged in successfully": "با موفقیت وارد شدید",
"Logged out successfully": "با موفقیت خارج شدید",
"MFA remember time": "MFA remember time",
"MFA remember time - Tooltip": "MFA remember time - Tooltip",
"Multiple Choices": "انتخاب‌های متعدد",
"New Application": "برنامه جدید",
"No verification": "بدون تأیید",
@@ -92,6 +96,7 @@
"Please input your application!": "لطفاً برنامه خود را وارد کنید!",
"Please input your organization!": "لطفاً سازمان خود را وارد کنید!",
"Please select a HTML file": "لطفاً یک فایل HTML انتخاب کنید",
"Pop up": "Pop up",
"Random": "تصادفی",
"Real name": "نام واقعی",
"Redirect URL": "آدرس بازگشت",
@@ -174,12 +179,27 @@
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD",
"USD": "USD"
"THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
},
"enforcer": {
"Edit Enforcer": "ویرایش Enforcer",
@@ -277,6 +297,7 @@
"Failed to save": "عدم موفقیت در ذخیره",
"Failed to sync": "عدم موفقیت در همگام‌سازی",
"Failed to verify": "عدم موفقیت در تأیید",
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "آدرس آیکون Favicon استفاده شده در تمام صفحات Casdoor سازمان",
"First name": "نام",
@@ -426,6 +447,7 @@
"Tokens": "توکن‌ها",
"Tour": "Tour",
"Transactions": "تراکنش‌ها",
"True": "True",
"Type": "نوع",
"Type - Tooltip": "نوع - راهنمای ابزار",
"URL": "آدرس",
@@ -550,6 +572,7 @@
"Please select an organization to sign in": "لطفاً یک سازمان برای ورود انتخاب کنید",
"Please type an organization to sign in": "لطفاً یک سازمان برای ورود تایپ کنید",
"Redirecting, please wait.": "در حال هدایت، لطفاً صبر کنید.",
"Refresh": "Refresh",
"Sign In": "ورود",
"Sign in with Face ID": "ورود با شناسه چهره",
"Sign in with WebAuthn": "ورود با WebAuthn",
@@ -563,6 +586,7 @@
"The input is not valid phone number!": "ورودی شماره تلفن معتبر نیست!",
"To access": "برای دسترسی",
"Verification code": "کد تأیید",
"WeChat": "WeChat",
"WebAuthn": "WebAuthn",
"sign up now": "ثبت‌نام کنید",
"username, Email or phone": "نام کاربری، ایمیل یا تلفن"
@@ -579,13 +603,13 @@
"Multi-factor recover": "بازیابی چندعاملی",
"Multi-factor recover description": "توضیح بازیابی چندعاملی",
"Or copy the secret to your Authenticator App": "یا راز را به برنامه تأیید هویت خود کپی کنید",
"Passcode": "کد عبور",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "لطفاً ابتدا ایمیل خود را متصل کنید، سیستم به‌طور خودکار از ایمیل برای احراز هویت چندعاملی استفاده می‌کند",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "لطفاً ابتدا تلفن خود را متصل کنید، سیستم به‌طور خودکار از تلفن برای احراز هویت چندعاملی استفاده می‌کند",
"Please confirm the information below": "لطفاً اطلاعات زیر را تأیید کنید",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "لطفاً این کد بازیابی را ذخیره کنید. هنگامی که دستگاه شما نتواند کد تأیید ارائه دهد، می‌توانید احراز هویت mfa را با این کد بازیابی تنظیم مجدد کنید",
"Protect your account with Multi-factor authentication": "حساب خود را با احراز هویت چندعاملی محافظت کنید",
"Recovery code": "کد بازیابی",
"Remember this account for {hour} hours": "Remember this account for {hour} hours",
"Scan the QR code with your Authenticator App": "کد QR را با برنامه تأیید هویت خود اسکن کنید",
"Set preferred": "تنظیم به‌عنوان مورد علاقه",
"Setup": "راه‌اندازی",
@@ -970,6 +994,8 @@
"Use WeChat Media Platform in PC - Tooltip": "آیا اجازه اسکن کد QR پلتفرم رسانه WeChat برای ورود داده شود",
"Use WeChat Media Platform to login": "استفاده از پلتفرم رسانه WeChat برای ورود",
"Use WeChat Open Platform to login": "استفاده از پلتفرم باز WeChat برای ورود",
"Use global endpoint": "Use global endpoint",
"Use global endpoint - Tooltip": "Use global endpoint - Tooltip",
"Use id as name": "Use id as name",
"Use id as name - Tooltip": "Use id as user's name",
"User flow": "جریان کاربر",

View File

@@ -75,12 +75,16 @@
"Header HTML - Edit": "Header HTML - Edit",
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"Inline": "Inline",
"Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code",
"Left": "Left",
"Logged in successfully": "Logged in successfully",
"Logged out successfully": "Logged out successfully",
"MFA remember time": "MFA remember time",
"MFA remember time - Tooltip": "MFA remember time - Tooltip",
"Multiple Choices": "Multiple Choices",
"New Application": "New Application",
"No verification": "No verification",
@@ -92,6 +96,7 @@
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file",
"Pop up": "Pop up",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Redirect URL",
@@ -174,12 +179,27 @@
"CAD": "CAD",
"CHF": "CHF",
"CNY": "CNY",
"CZK": "CZK",
"DKK": "DKK",
"EUR": "EUR",
"GBP": "GBP",
"HKD": "HKD",
"HUF": "HUF",
"INR": "INR",
"JPY": "JPY",
"KRW": "KRW",
"MXN": "MXN",
"MYR": "MYR",
"NOK": "NOK",
"PLN": "PLN",
"RUB": "RUB",
"SEK": "SEK",
"SGD": "SGD",
"USD": "USD"
"THB": "THB",
"TRY": "TRY",
"TWD": "TWD",
"USD": "USD",
"ZAR": "ZAR"
},
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
@@ -277,6 +297,7 @@
"Failed to save": "Failed to save",
"Failed to sync": "Failed to sync",
"Failed to verify": "Failed to verify",
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
"First name": "First name",
@@ -426,6 +447,7 @@
"Tokens": "Tokens",
"Tour": "Tour",
"Transactions": "Transactions",
"True": "True",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
@@ -550,6 +572,7 @@
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
"Redirecting, please wait.": "Redirecting, please wait.",
"Refresh": "Refresh",
"Sign In": "Sign In",
"Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@@ -563,6 +586,7 @@
"The input is not valid phone number!": "The input is not valid phone number!",
"To access": "To access",
"Verification code": "Verification code",
"WeChat": "WeChat",
"WebAuthn": "WebAuthn",
"sign up now": "sign up now",
"username, Email or phone": "username, Email or phone"
@@ -579,13 +603,13 @@
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please confirm the information below": "Please confirm the information below",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Remember this account for {hour} hours": "Remember this account for {hour} hours",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
@@ -970,6 +994,8 @@
"Use WeChat Media Platform in PC - Tooltip": "Whether to allow scanning WeChat Media Platform QR code to login",
"Use WeChat Media Platform to login": "Use WeChat Media Platform to login",
"Use WeChat Open Platform to login": "Use WeChat Open Platform to login",
"Use global endpoint": "Use global endpoint",
"Use global endpoint - Tooltip": "Use global endpoint - Tooltip",
"Use id as name": "Use id as name",
"Use id as name - Tooltip": "Use id as user's name",
"User flow": "User flow",

Some files were not shown because too many files have changed in this diff Show More