Compare commits

...

8 Commits

55 changed files with 225 additions and 116 deletions

View File

@ -42,6 +42,7 @@ type Response struct {
Name string `json:"name"` Name string `json:"name"`
Data interface{} `json:"data"` Data interface{} `json:"data"`
Data2 interface{} `json:"data2"` Data2 interface{} `json:"data2"`
Data3 interface{} `json:"data3"`
} }
type Captcha struct { type Captcha struct {

View File

@ -132,7 +132,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
if form.Type == ResponseTypeLogin { if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId) c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", 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 { } else if form.Type == ResponseTypeCode {
clientId := c.Input().Get("clientId") clientId := c.Input().Get("clientId")
responseType := c.Input().Get("responseType") responseType := c.Input().Get("responseType")
@ -154,7 +154,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
} }
resp = codeToResponse(code) resp = codeToResponse(code)
resp.Data2 = user.NeedUpdatePassword resp.Data3 = user.NeedUpdatePassword
if application.EnableSigninSession || application.HasPromptPage() { if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in // The prompt page needs the user to be signed in
c.SetSessionUsername(userId) 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) token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token) resp = tokenToResponse(token)
resp.Data2 = user.NeedUpdatePassword resp.Data3 = user.NeedUpdatePassword
} }
} else if form.Type == ResponseTypeDevice { } else if form.Type == ResponseTypeDevice {
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode) 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) 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 } else if form.Type == ResponseTypeSaml { // saml flow
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host) res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
if err != nil { if err != nil {
c.ResponseError(err.Error(), nil) c.ResponseError(err.Error(), nil)
return 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() { if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in // The prompt page needs the user to be signed in
@ -555,8 +555,11 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application")) c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application"))
return return
} }
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
var enableCaptcha bool 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()) c.ResponseError(err.Error())
return return
} else if enableCaptcha { } else if enableCaptcha {
@ -1222,27 +1225,26 @@ func (c *ApiController) GetQRCode() {
func (c *ApiController) GetCaptchaStatus() { func (c *ApiController) GetCaptchaStatus() {
organization := c.Input().Get("organization") organization := c.Input().Get("organization")
userId := c.Input().Get("userId") 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 { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if application == nil {
captchaEnabled := false c.ResponseError("application not found")
if user != nil { return
var failedSigninLimit int
failedSigninLimit, _, err = object.GetFailedSigninConfigByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
if user.SigninWrongTimes >= failedSigninLimit {
captchaEnabled = true
}
} }
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) c.ResponseOk(captchaEnabled)
return
} }
// Callback // Callback

View File

@ -574,7 +574,7 @@ func (c *ApiController) SetPassword() {
targetUser.LastChangePasswordTime = util.GetCurrentTime() targetUser.LastChangePasswordTime = util.GetCurrentTime()
if user.Ldap == "" { 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 { } else {
if isAdmin { if isAdmin {
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage()) err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())

View File

@ -23,7 +23,7 @@ func NewArgon2idCredManager() *Argon2idCredManager {
return cm 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) hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil { if err != nil {
return "" return ""
@ -31,7 +31,7 @@ func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt strin
return hash 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) match, _ := argon2id.ComparePasswordAndHash(plainPwd, hashedPwd)
return match return match
} }

View File

@ -9,7 +9,7 @@ func NewBcryptCredManager() *BcryptCredManager {
return cm 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) bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
return "" return ""
@ -17,7 +17,7 @@ func (cm *BcryptCredManager) GetHashedPassword(password string, userSalt string,
return string(bytes) 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)) err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(plainPwd))
return err == nil return err == nil
} }

View File

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

View File

@ -37,14 +37,10 @@ func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
return cm return cm
} }
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string { func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, salt string) string {
res := getMd5HexDigest(password) return getMd5HexDigest(getMd5HexDigest(password) + salt)
if userSalt != "" {
res = getMd5HexDigest(res + userSalt)
}
return res
} }
func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool { func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt) return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
} }

View File

@ -28,13 +28,13 @@ func NewPbkdf2SaltCredManager() *Pbkdf2SaltCredManager {
return cm 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 // 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) res := pbkdf2.Key([]byte(password), decodedSalt, 27500, 64, sha256.New)
return base64.StdEncoding.EncodeToString(res) return base64.StdEncoding.EncodeToString(res)
} }
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool { func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt) return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
} }

View File

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

View File

@ -21,10 +21,10 @@ func NewPlainCredManager() *PlainCredManager {
return cm return cm
} }
func (cm *PlainCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string { func (cm *PlainCredManager) GetHashedPassword(password string, salt string) string {
return password 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 return hashedPwd == plainPwd
} }

View File

@ -37,14 +37,10 @@ func NewSha256SaltCredManager() *Sha256SaltCredManager {
return cm return cm
} }
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string { func (cm *Sha256SaltCredManager) GetHashedPassword(password string, salt string) string {
res := getSha256HexDigest(password) return getSha256HexDigest(getSha256HexDigest(password) + salt)
if organizationSalt != "" {
res = getSha256HexDigest(res + organizationSalt)
}
return res
} }
func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool { func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt) return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
} }

View File

@ -23,12 +23,12 @@ func TestGetSaltedPassword(t *testing.T) {
password := "123456" password := "123456"
salt := "123" salt := "123"
cm := NewSha256SaltCredManager() 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) { func TestGetPassword(t *testing.T) {
password := "123456" password := "123456"
cm := NewSha256SaltCredManager() cm := NewSha256SaltCredManager()
// https://passwordsgenerator.net/sha256-hash-generator/ // 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,10 @@ func NewSha512SaltCredManager() *Sha512SaltCredManager {
return cm return cm
} }
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string { func (cm *Sha512SaltCredManager) GetHashedPassword(password string, salt string) string {
res := getSha512HexDigest(password) return getSha512HexDigest(getSha512HexDigest(password) + salt)
if organizationSalt != "" {
res = getSha512HexDigest(res + organizationSalt)
}
return res
} }
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool { func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt) return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
} }

View File

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

View File

@ -252,12 +252,12 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
credManager := cred.GetCredManager(passwordType) credManager := cred.GetCredManager(passwordType)
if credManager != nil { if credManager != nil {
if organization.MasterPassword != "" { if organization.MasterPassword != "" {
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) { if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, organization.PasswordSalt) {
return resetUserSigninErrorTimes(user) return resetUserSigninErrorTimes(user)
} }
} }
if credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt, organization.PasswordSalt) { if credManager.IsPasswordCorrect(password, user.Password, organization.PasswordSalt) || credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt) {
return resetUserSigninErrorTimes(user) return resetUserSigninErrorTimes(user)
} }
@ -593,31 +593,41 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
return "" 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 { if len(application.Providers) == 0 {
return false, nil return false, nil
} }
for _, providerItem := range application.Providers { for _, providerItem := range application.Providers {
if providerItem.Provider == nil { if providerItem.Provider == nil || providerItem.Provider.Category != "Captcha" {
continue continue
} }
if providerItem.Provider.Category == "Captcha" {
if providerItem.Rule == "Dynamic" { if providerItem.Rule == "Internet-Only" {
user, err := GetUserByFields(organization, username) 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 { if err != nil {
return false, err return false, err
} }
failedSigninLimit := application.FailedSigninLimit return user.SigninWrongTimes >= failedSigninLimit, nil
if failedSigninLimit == 0 {
failedSigninLimit = DefaultFailedSigninLimit
}
return user != nil && user.SigninWrongTimes >= failedSigninLimit, nil
} }
return providerItem.Rule == "Always", nil
return false, nil
} }
return providerItem.Rule == "Always", nil
} }
return false, nil return false, nil

View File

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

View File

@ -222,7 +222,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
if organization.MasterPassword != "" && organization.MasterPassword != "***" { if organization.MasterPassword != "" && organization.MasterPassword != "***" {
credManager := cred.GetCredManager(organization.PasswordType) credManager := cred.GetCredManager(organization.PasswordType)
if credManager != nil { if credManager != nil {
hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, "", organization.PasswordSalt) hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, organization.PasswordSalt)
organization.MasterPassword = hashedPassword organization.MasterPassword = hashedPassword
} }
} }
@ -536,7 +536,13 @@ func IsNeedPromptMfa(org *Organization, user *User) bool {
if org == nil || user == nil { if org == nil || user == nil {
return false 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.Rule == "Required" {
if item.Name == EmailType && !user.MfaEmailEnabled { if item.Name == EmailType && !user.MfaEmailEnabled {
return true return true

View File

@ -212,6 +212,7 @@ type User struct {
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"` MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
NeedUpdatePassword bool `json:"needUpdatePassword"` NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"` IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
} }
@ -795,7 +796,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
} }
} }
if isAdmin { 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") columns = append(columns, "updated_time")

View File

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

View File

@ -185,17 +185,3 @@ func removePort(s string) string {
} }
return ipStr 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) setCorsHeaders(ctx, origin)
} else if originHostname == host { } else if originHostname == host {
setCorsHeaders(ctx, origin) setCorsHeaders(ctx, origin)
} else if isHostIntranet(host) { } else if util.IsHostIntranet(host) {
setCorsHeaders(ctx, origin) setCorsHeaders(ctx, origin)
} else { } else {
ok, err := object.IsOriginAllowed(origin) 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

@ -696,18 +696,27 @@ export const MfaRulePrompted = "Prompted";
export const MfaRuleOptional = "Optional"; export const MfaRuleOptional = "Optional";
export function isRequiredEnableMfa(user, organization) { export function isRequiredEnableMfa(user, organization) {
if (!user || !organization || !organization.mfaItems) { if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
return false; return false;
} }
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0; return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
} }
export function getMfaItemsByRules(user, organization, mfaRules = []) { export function getMfaItemsByRules(user, organization, mfaRules = []) {
if (!user || !organization || !organization.mfaItems) { if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
return []; 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)); .filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
} }

View File

@ -42,6 +42,7 @@ import * as MfaBackend from "./backend/MfaBackend";
import AccountAvatar from "./account/AccountAvatar"; import AccountAvatar from "./account/AccountAvatar";
import FaceIdTable from "./table/FaceIdTable"; import FaceIdTable from "./table/FaceIdTable";
import MfaAccountTable from "./table/MfaAccountTable"; import MfaAccountTable from "./table/MfaAccountTable";
import MfaTable from "./table/MfaTable";
const {Option} = Select; const {Option} = Select;
@ -926,6 +927,19 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </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") { } else if (accountItem.name === "Multi-factor authentication") {
return ( return (
!this.isSelfOrAdmin() ? null : ( !this.isSelfOrAdmin() ? null : (

View File

@ -163,7 +163,7 @@ export function getWechatQRCode(providerId) {
} }
export function getCaptchaStatus(values) { 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", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@ -166,7 +166,7 @@ class AuthCallback extends React.Component {
const responseType = this.getResponseType(); const responseType = this.getResponseType();
const handleLogin = (res) => { const handleLogin = (res) => {
if (responseType === "login") { if (responseType === "login") {
if (res.data2) { if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl); sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`); Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return; return;
@ -176,7 +176,7 @@ class AuthCallback extends React.Component {
const link = Setting.getFromLink(); const link = Setting.getFromLink();
Setting.goToLink(link); Setting.goToLink(link);
} else if (responseType === "code") { } else if (responseType === "code") {
if (res.data2) { if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl); sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`); Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return; return;
@ -185,7 +185,7 @@ class AuthCallback extends React.Component {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`); Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
// Setting.showMessage("success", `Authorization code: ${res.data}`); // Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") { } else if (responseType === "token" || responseType === "id_token") {
if (res.data2) { if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl); sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`); Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return; return;
@ -207,7 +207,7 @@ class AuthCallback extends React.Component {
relayState: oAuthParams.relayState, relayState: oAuthParams.relayState,
}); });
} else { } else {
if (res.data2.needUpdatePassword) { if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl); sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`); Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return; return;

View File

@ -134,6 +134,8 @@ class LoginPage extends React.Component {
return CaptchaRule.Always; return CaptchaRule.Always;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) { } else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) {
return CaptchaRule.Dynamic; return CaptchaRule.Dynamic;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Internet-Only")) {
return CaptchaRule.InternetOnly;
} else { } else {
return CaptchaRule.Never; return CaptchaRule.Never;
} }
@ -443,6 +445,9 @@ class LoginPage extends React.Component {
} else if (captchaRule === CaptchaRule.Dynamic) { } else if (captchaRule === CaptchaRule.Dynamic) {
this.checkCaptchaStatus(values); this.checkCaptchaStatus(values);
return; return;
} else if (captchaRule === CaptchaRule.InternetOnly) {
this.checkCaptchaStatus(values);
return;
} }
} }
this.login(values); this.login(values);
@ -491,9 +496,9 @@ class LoginPage extends React.Component {
const responseType = values["type"]; const responseType = values["type"];
if (responseType === "login") { if (responseType === "login") {
if (res.data2) { if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search); 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")); Setting.showMessage("success", i18next.t("application:Logged in successfully"));
this.props.onLoginSuccess(); this.props.onLoginSuccess();
@ -505,9 +510,9 @@ class LoginPage extends React.Component {
userCodeStatus: "success", userCodeStatus: "success",
}); });
} else if (responseType === "token" || responseType === "id_token") { } else if (responseType === "token" || responseType === "id_token") {
if (res.data2) { if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search); 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 amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
const accessToken = res.data; const accessToken = res.data;
@ -517,9 +522,9 @@ class LoginPage extends React.Component {
this.props.onLoginSuccess(window.location.href); this.props.onLoginSuccess(window.location.href);
return; return;
} }
if (res.data2.needUpdatePassword) { if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search); 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") { if (res.data2.method === "POST") {
this.setState({ this.setState({
@ -961,9 +966,23 @@ class LoginPage extends React.Component {
const captchaProviderItems = this.getCaptchaProviderItems(application); const captchaProviderItems = this.getCaptchaProviderItems(application);
const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always"); const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always");
const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic"); const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic");
const provider = alwaysProviderItems.length > 0 const internetOnlyProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Internet-Only");
? alwaysProviderItems[0].provider
: dynamicProviderItems[0].provider; // 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 return <CaptchaModal
owner={provider.owner} owner={provider.owner}

View File

@ -181,4 +181,5 @@ export const CaptchaRule = {
Always: "Always", Always: "Always",
Never: "Never", Never: "Never",
Dynamic: "Dynamic", Dynamic: "Dynamic",
InternetOnly: "Internet-Only",
}; };

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Přizpůsobit hlavičku vstupní stránky vaší aplikace", "Header HTML - Tooltip": "Přizpůsobit hlavičku vstupní stránky vaší aplikace",
"Incremental": "Inkrementální", "Incremental": "Inkrementální",
"Input": "Vstup", "Input": "Vstup",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Kód pozvánky", "Invitation code": "Kód pozvánky",
"Left": "Vlevo", "Left": "Vlevo",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Links", "Left": "Links",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Izquierda", "Left": "Izquierda",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "کد head صفحه ورود برنامه خود را سفارشی کنید", "Header HTML - Tooltip": "کد head صفحه ورود برنامه خود را سفارشی کنید",
"Incremental": "افزایشی", "Incremental": "افزایشی",
"Input": "ورودی", "Input": "ورودی",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "کد دعوت", "Invitation code": "کد دعوت",
"Left": "چپ", "Left": "چپ",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incrémentale", "Incremental": "Incrémentale",
"Input": "Saisie", "Input": "Saisie",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Code d'invitation", "Invitation code": "Code d'invitation",
"Left": "Gauche", "Left": "Gauche",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Kiri", "Left": "Kiri",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "左", "Left": "左",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "왼쪽", "Left": "왼쪽",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Código de convite", "Invitation code": "Código de convite",
"Left": "Esquerda", "Left": "Esquerda",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Последовательный", "Incremental": "Последовательный",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Код приглашения", "Invitation code": "Код приглашения",
"Left": "Левый", "Left": "Левый",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Vlastný HTML kód pre hlavičku vašej vstupnej stránky aplikácie", "Header HTML - Tooltip": "Vlastný HTML kód pre hlavičku vašej vstupnej stránky aplikácie",
"Incremental": "Postupný", "Incremental": "Postupný",
"Input": "Vstup", "Input": "Vstup",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Kód pozvania", "Invitation code": "Kód pozvania",
"Left": "Vľavo", "Left": "Vľavo",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Davet Kodu", "Invitation code": "Davet Kodu",
"Left": "Sol", "Left": "Sol",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Налаштуйте тег head на сторінці входу до програми", "Header HTML - Tooltip": "Налаштуйте тег head на сторінці входу до програми",
"Incremental": "Інкрементний", "Incremental": "Інкрементний",
"Input": "Введення", "Input": "Введення",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Код запрошення", "Invitation code": "Код запрошення",
"Left": "Ліворуч", "Left": "Ліворуч",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Tăng", "Incremental": "Tăng",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Trái", "Left": "Trái",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "自定义应用页面的head标签", "Header HTML - Tooltip": "自定义应用页面的head标签",
"Incremental": "递增", "Incremental": "递增",
"Input": "输入", "Input": "输入",
"Internet-Only": "外网启用",
"Invalid characters in application name": "应用名称内有非法字符", "Invalid characters in application name": "应用名称内有非法字符",
"Invitation code": "邀请码", "Invitation code": "邀请码",
"Left": "居左", "Left": "居左",

View File

@ -110,6 +110,7 @@ class AccountTable extends React.Component {
{name: "Managed accounts", label: i18next.t("user:Managed accounts")}, {name: "Managed accounts", label: i18next.t("user:Managed accounts")},
{name: "Face ID", label: i18next.t("user:Face ID")}, {name: "Face ID", label: i18next.t("user:Face ID")},
{name: "MFA accounts", label: i18next.t("user:MFA accounts")}, {name: "MFA accounts", label: i18next.t("user:MFA accounts")},
{name: "MFA items", label: i18next.t("general:MFA items")},
]; ];
}; };

View File

@ -255,6 +255,7 @@ class ProviderTable extends React.Component {
<Option key="None" value="None">{i18next.t("general:None")}</Option> <Option key="None" value="None">{i18next.t("general:None")}</Option>
<Option key="Dynamic" value="Dynamic">{i18next.t("application:Dynamic")}</Option> <Option key="Dynamic" value="Dynamic">{i18next.t("application:Dynamic")}</Option>
<Option key="Always" value="Always">{i18next.t("application:Always")}</Option> <Option key="Always" value="Always">{i18next.t("application:Always")}</Option>
<Option key="Internet-Only" value="Internet-Only">{i18next.t("application:Internet-Only")}</Option>
</Select> </Select>
); );
} else if (record.provider?.category === "SMS" || record.provider?.category === "Email") { } else if (record.provider?.category === "SMS" || record.provider?.category === "Email") {