Compare commits

...

31 Commits

Author SHA1 Message Date
8e71e23d75 feat: improve error message for GetConfigInt64() 2025-07-09 00:32:00 +08:00
146a369f80 feat: improve error handling in AutoSigninFilter 2025-07-08 23:47:14 +08:00
9bbe5afb7c feat: use only one salt arg in CredManager.IsPasswordCorrect() (#3936) 2025-07-07 17:56:25 +08:00
b42391c6ce feat: move needUpdatePassword to response's Data3 field to avoid refresh token conflict (#3931) 2025-07-05 22:48:44 +08:00
fb035a5353 feat: CredManager.GetHashedPassword() only contains one salt arg now (#3928) 2025-07-05 18:41:37 +08:00
b1f68a60a4 feat: set createDatabase to false in TestDumpToFile() (#3924) 2025-07-03 22:50:23 +08:00
201d704a31 feat: improve TikTok username generation logic (#3923) 2025-07-03 20:53:15 +08:00
bf91ad6c97 feat: add Internet-Only captcha rule (#3919) 2025-07-03 02:39:06 +08:00
3ccc0339c7 feat: improve CheckToEnableCaptcha() logic 2025-07-03 02:32:07 +08:00
1f2b0a3587 feat: add user's MFA items (#3921) 2025-07-02 23:05:07 +08:00
0b3feb0d5f feat: use Input.OTP to input totp code (#3922) 2025-07-02 18:22:59 +08:00
568c0e2c3d feat: show Organization.PasswordOptions in login UI (#3913) 2025-06-28 22:13:00 +08:00
f4ad2b4034 feat: remove "@" from name's forbidden chars 2025-06-27 18:41:50 +08:00
c9f8727890 feat: fix bug in InitCleanupTokens() (#3910) 2025-06-27 02:08:18 +08:00
e2e3c1fbb8 feat: support Product.SuccessUrl (#3908) 2025-06-26 22:52:07 +08:00
73915ac0a0 feat: fix issue that LDAP user address was not syncing (#3905) 2025-06-26 09:38:16 +08:00
bf9d55ff40 feat: add InitCleanupTokens() (#3903) 2025-06-26 09:31:59 +08:00
b36fb50239 feat: fix check bug to allow logged-in users to buy product (#3897) 2025-06-25 10:49:20 +08:00
4307baa759 feat: fix Tumblr OAuth's wrong scope (#3898) 2025-06-25 09:55:02 +08:00
3964bae1df feat: fix org's LDAP table wrong link (#3900) 2025-06-25 09:51:40 +08:00
d9b97d70be feat: change CRLF to LF for some files 2025-06-24 09:55:00 +08:00
ca224fdd4c feat: add group xlsx upload button (#3885) 2025-06-17 23:43:38 +08:00
37daea2bbc feat: improve error message in ApplicationEditPage (#3886) 2025-06-17 20:06:52 +08:00
af231bf946 feat: add FieldValidationFilter to check object names (#3877) 2025-06-17 16:11:35 +08:00
6dc7b4d533 feat: get-user API respects org's account item's view rules now (#3882) 2025-06-16 20:09:21 +08:00
12cc0f429e feat: remove support for Non trace verification for Alibaba cloud captcha verification (#3881) 2025-06-13 00:36:29 +08:00
8cc22dec91 feat: upgrade Alibaba cloud captcha provider from v1 to v2 (#3879) 2025-06-12 23:02:36 +08:00
0c08ae5365 feat: Add support for email verification logic (#3875) 2025-06-11 19:17:16 +08:00
c3485268d3 feat: fix "Display name cannot be empty" in /update-user API 2025-06-11 00:32:05 +08:00
64a4956c42 feat: improve getMemoryUsage() 2025-06-09 20:08:55 +08:00
855bdf47e8 feat: fix memory usage in sysinfo page (#3870) 2025-06-09 00:31:34 +08:00
130 changed files with 2173 additions and 1471 deletions

View File

@ -15,32 +15,51 @@
package captcha
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/casdoor/casdoor/util"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
openapiutil "github.com/alibabacloud-go/openapi-util/service"
teaUtil "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
const AliyunCaptchaVerifyUrl = "http://afs.aliyuncs.com"
const AliyunCaptchaVerifyUrl = "captcha.cn-shanghai.aliyuncs.com"
type captchaSuccessResponse struct {
Code int `json:"Code"`
Msg string `json:"Msg"`
type VerifyCaptchaRequest struct {
CaptchaVerifyParam *string `json:"CaptchaVerifyParam,omitempty" xml:"CaptchaVerifyParam,omitempty"`
SceneId *string `json:"SceneId,omitempty" xml:"SceneId,omitempty"`
}
type captchaFailResponse struct {
Code string `json:"Code"`
Message string `json:"Message"`
type VerifyCaptchaResponseBodyResult struct {
VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"`
}
type VerifyCaptchaResponseBody struct {
Code *string `json:"Code,omitempty" xml:"Code,omitempty"`
Message *string `json:"Message,omitempty" xml:"Message,omitempty"`
// Id of the request
RequestId *string `json:"RequestId,omitempty" xml:"RequestId,omitempty"`
Result *VerifyCaptchaResponseBodyResult `json:"Result,omitempty" xml:"Result,omitempty" type:"Struct"`
Success *bool `json:"Success,omitempty" xml:"Success,omitempty"`
}
type VerifyIntelligentCaptchaResponseBodyResult struct {
VerifyCode *string `json:"VerifyCode,omitempty" xml:"VerifyCode,omitempty"`
VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"`
}
type VerifyIntelligentCaptchaResponseBody struct {
Code *string `json:"Code,omitempty" xml:"Code,omitempty"`
Message *string `json:"Message,omitempty" xml:"Message,omitempty"`
// Id of the request
RequestId *string `json:"RequestId,omitempty" xml:"RequestId,omitempty"`
Result *VerifyIntelligentCaptchaResponseBodyResult `json:"Result,omitempty" xml:"Result,omitempty" type:"Struct"`
Success *bool `json:"Success,omitempty" xml:"Success,omitempty"`
}
type VerifyIntelligentCaptchaResponse struct {
Headers map[string]*string `json:"headers,omitempty" xml:"headers,omitempty" require:"true"`
StatusCode *int32 `json:"statusCode,omitempty" xml:"statusCode,omitempty" require:"true"`
Body *VerifyIntelligentCaptchaResponseBody `json:"body,omitempty" xml:"body,omitempty" require:"true"`
}
type AliyunCaptchaProvider struct{}
func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
@ -48,68 +67,69 @@ func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
return captcha
}
func contentEscape(str string) string {
str = strings.Replace(str, " ", "%20", -1)
str = url.QueryEscape(str)
return str
}
func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
config := &openapi.Config{}
func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
pathData, err := url.ParseQuery(token)
config.Endpoint = tea.String(AliyunCaptchaVerifyUrl)
config.ConnectTimeout = tea.Int(5000)
config.ReadTimeout = tea.Int(5000)
config.AccessKeyId = tea.String(clientId)
config.AccessKeySecret = tea.String(clientSecret)
client := new(openapi.Client)
err := client.Init(config)
if err != nil {
return false, err
}
pathData["Action"] = []string{"AuthenticateSig"}
pathData["Format"] = []string{"json"}
pathData["SignatureMethod"] = []string{"HMAC-SHA1"}
pathData["SignatureNonce"] = []string{strconv.FormatInt(time.Now().UnixNano(), 10)}
pathData["SignatureVersion"] = []string{"1.0"}
pathData["Timestamp"] = []string{time.Now().UTC().Format("2006-01-02T15:04:05Z")}
pathData["Version"] = []string{"2018-01-12"}
request := VerifyCaptchaRequest{CaptchaVerifyParam: tea.String(token), SceneId: tea.String(clientId2)}
var keys []string
for k := range pathData {
keys = append(keys, k)
}
sort.Strings(keys)
sortQuery := ""
for _, k := range keys {
sortQuery += k + "=" + contentEscape(pathData[k][0]) + "&"
}
sortQuery = strings.TrimSuffix(sortQuery, "&")
stringToSign := fmt.Sprintf("GET&%s&%s", url.QueryEscape("/"), url.QueryEscape(sortQuery))
signature := util.GetHmacSha1(clientSecret+"&", stringToSign)
resp, err := http.Get(fmt.Sprintf("%s?%s&Signature=%s", AliyunCaptchaVerifyUrl, sortQuery, url.QueryEscape(signature)))
err = teaUtil.ValidateModel(&request)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
runtime := &teaUtil.RuntimeOptions{}
body := map[string]interface{}{}
if !tea.BoolValue(teaUtil.IsUnset(request.CaptchaVerifyParam)) {
body["CaptchaVerifyParam"] = request.CaptchaVerifyParam
}
if !tea.BoolValue(teaUtil.IsUnset(request.SceneId)) {
body["SceneId"] = request.SceneId
}
req := &openapi.OpenApiRequest{
Body: openapiutil.ParseToMap(body),
}
params := &openapi.Params{
Action: tea.String("VerifyIntelligentCaptcha"),
Version: tea.String("2023-03-05"),
Protocol: tea.String("HTTPS"),
Pathname: tea.String("/"),
Method: tea.String("POST"),
AuthType: tea.String("AK"),
Style: tea.String("RPC"),
ReqBodyType: tea.String("formData"),
BodyType: tea.String("json"),
}
res := &VerifyIntelligentCaptchaResponse{}
resBody, err := client.CallApi(params, req, runtime)
if err != nil {
return false, err
}
return handleCaptchaResponse(body)
}
func handleCaptchaResponse(body []byte) (bool, error) {
captchaResp := &captchaSuccessResponse{}
err := json.Unmarshal(body, captchaResp)
err = tea.Convert(resBody, &res)
if err != nil {
captchaFailResp := &captchaFailResponse{}
err = json.Unmarshal(body, captchaFailResp)
if err != nil {
return false, err
}
return false, errors.New(captchaFailResp.Message)
return false, err
}
return true, nil
if res.Body.Result.VerifyResult != nil && *res.Body.Result.VerifyResult {
return true, nil
}
return false, nil
}

View File

@ -23,6 +23,6 @@ func NewDefaultCaptchaProvider() *DefaultCaptchaProvider {
return captcha
}
func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
return object.VerifyCaptcha(clientSecret, token), nil
}

View File

@ -35,7 +35,7 @@ func NewGEETESTCaptchaProvider() *GEETESTCaptchaProvider {
return captcha
}
func (captcha *GEETESTCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
func (captcha *GEETESTCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
pathData, err := url.ParseQuery(token)
if err != nil {
return false, err

View File

@ -32,7 +32,7 @@ func NewHCaptchaProvider() *HCaptchaProvider {
return captcha
}
func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},

View File

@ -17,7 +17,7 @@ package captcha
import "fmt"
type CaptchaProvider interface {
VerifyCaptcha(token, clientSecret string) (bool, error)
VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error)
}
func GetCaptchaProvider(captchaType string) CaptchaProvider {
@ -43,11 +43,11 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
return nil
}
func VerifyCaptchaByCaptchaType(captchaType, token, clientSecret string) (bool, error) {
func VerifyCaptchaByCaptchaType(captchaType, token, clientId, clientSecret, clientId2 string) (bool, error) {
provider := GetCaptchaProvider(captchaType)
if provider == nil {
return false, fmt.Errorf("invalid captcha provider: %s", captchaType)
}
return provider.VerifyCaptcha(token, clientSecret)
return provider.VerifyCaptcha(token, clientId, clientSecret, clientId2)
}

View File

@ -32,7 +32,7 @@ func NewReCaptchaProvider() *ReCaptchaProvider {
return captcha
}
func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},

View File

@ -32,7 +32,7 @@ func NewCloudflareTurnstileProvider() *CloudflareTurnstileProvider {
return captcha
}
func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},

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 {

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
@ -504,6 +504,8 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), authForm.CountryCode))
return
}
} else if verificationCodeType == object.VerifyTypeEmail {
checkDest = authForm.Username
}
// check result through Email or Phone
@ -553,8 +555,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 {
@ -569,7 +574,7 @@ func (c *ApiController) Login() {
}
var isHuman bool
isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, captchaProvider.ClientId, authForm.ClientSecret, captchaProvider.ClientId2)
if err != nil {
c.ResponseError(err.Error())
return
@ -1220,27 +1225,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

@ -0,0 +1,56 @@
// 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 controllers
import (
"fmt"
"os"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func (c *ApiController) UploadGroups() {
userId := c.GetSessionUsername()
owner, user := util.GetOwnerAndNameFromId(userId)
file, header, err := c.Ctx.Request.FormFile("file")
if err != nil {
c.ResponseError(err.Error())
return
}
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId)
defer os.Remove(path)
err = saveFile(path, &file)
if err != nil {
c.ResponseError(err.Error())
return
}
affected, err := object.UploadGroups(owner, path)
if err != nil {
c.ResponseError(err.Error())
return
}
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("general:Failed to import groups"))
}
}

View File

@ -49,6 +49,6 @@ func (c *ApiController) UploadPermissions() {
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("user_upload:Failed to import users"))
c.ResponseError(c.T("general:Failed to import users"))
}
}

View File

@ -182,7 +182,7 @@ func (c *ApiController) BuyProduct() {
paidUserName := c.Input().Get("userName")
owner, _ := util.GetOwnerAndNameFromId(id)
userId := util.GetId(owner, paidUserName)
if paidUserName != "" && !c.IsAdmin() {
if paidUserName != "" && paidUserName != c.GetSessionUsername() && !c.IsAdmin() {
c.ResponseError(c.T("general:Only admin user can specify user"))
return
}

View File

@ -49,6 +49,6 @@ func (c *ApiController) UploadRoles() {
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("user_upload:Failed to import users"))
c.ResponseError(c.T("general:Failed to import users"))
}
}

View File

@ -197,8 +197,8 @@ func (c *ApiController) GetUser() {
return
}
var organization *object.Organization
if user != nil {
var organization *object.Organization
organization, err = object.GetOrganizationByUser(user)
if err != nil {
c.ResponseError(err.Error())
@ -237,6 +237,14 @@ func (c *ApiController) GetUser() {
return
}
if organization != nil && user != nil {
user, err = object.GetFilteredUser(user, c.IsAdmin(), c.IsAdminOrSelf(user), organization.AccountItems)
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.ResponseOk(user)
}
@ -282,13 +290,6 @@ func (c *ApiController) UpdateUser() {
return
}
if c.Input().Get("allowEmpty") == "" {
if user.DisplayName == "" {
c.ResponseError(c.T("user:Display name cannot be empty"))
return
}
}
if user.MfaEmailEnabled && user.Email == "" {
c.ResponseError(c.T("user:MFA email is enabled but email is empty"))
return
@ -310,7 +311,8 @@ func (c *ApiController) UpdateUser() {
}
isAdmin := c.IsAdmin()
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, c.GetAcceptLanguage()); !pass {
allowDisplayNameEmpty := c.Input().Get("allowEmpty") != ""
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, allowDisplayNameEmpty, c.GetAcceptLanguage()); !pass {
c.ResponseError(err)
return
}
@ -572,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

@ -67,6 +67,6 @@ func (c *ApiController) UploadUsers() {
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("user_upload:Failed to import users"))
c.ResponseError(c.T("general:Failed to import users"))
}
}

View File

@ -160,7 +160,7 @@ func (c *ApiController) SendVerificationCode() {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, provider.ClientId, vform.ClientSecret, provider.ClientId2); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
@ -349,7 +349,7 @@ func (c *ApiController) VerifyCaptcha() {
return
}
isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret)
isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2)
if err != nil {
c.ResponseError(err.Error())
return

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,10 @@ 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)
}
return res
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, salt string) string {
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 {
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,10 @@ 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)
}
return res
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, salt string) string {
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 {
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,10 @@ 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)
}
return res
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, salt string) string {
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 {
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
}

2
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
github.com/alibabacloud-go/openapi-util v0.1.0
github.com/alibabacloud-go/tea v1.3.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/aws/aws-sdk-go v1.45.5
@ -90,7 +91,6 @@ require (
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Nepodařilo se importovat uživatele",
"Missing parameter": "Chybějící parametr",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Prosím, přihlaste se nejprve",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Nové heslo nemůže obsahovat prázdné místo.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Nepodařilo se importovat uživatele"
},
"util": {
"No application is found for userId: %s": "Pro userId: %s nebyla nalezena žádná aplikace",
"No provider for category: %s is found for application: %s": "Pro kategorii: %s nebyl nalezen žádný poskytovatel pro aplikaci: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Fehler beim Importieren von Benutzern",
"Missing parameter": "Fehlender Parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Bitte zuerst einloggen",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Fehler beim Importieren von Benutzern"
},
"util": {
"No application is found for userId: %s": "Es wurde keine Anwendung für die Benutzer-ID gefunden: %s",
"No provider for category: %s is found for application: %s": "Kein Anbieter für die Kategorie %s gefunden für die Anwendung: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Error al importar usuarios",
"Missing parameter": "Parámetro faltante",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Por favor, inicia sesión primero",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Error al importar usuarios"
},
"util": {
"No application is found for userId: %s": "No se encuentra ninguna aplicación para el Id de usuario: %s",
"No provider for category: %s is found for application: %s": "No se encuentra un proveedor para la categoría: %s para la aplicación: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "عدم موفقیت در وارد کردن کاربران",
"Missing parameter": "پارامتر گمشده",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "لطفاً ابتدا وارد شوید",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "رمز عبور جدید نمی‌تواند حاوی فاصله خالی باشد.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "عدم موفقیت در وارد کردن کاربران"
},
"util": {
"No application is found for userId: %s": "هیچ برنامه‌ای برای userId: %s یافت نشد",
"No provider for category: %s is found for application: %s": "هیچ ارائه‌دهنده‌ای برای دسته‌بندی: %s برای برنامه: %s یافت نشد",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Échec de l'importation des utilisateurs",
"Missing parameter": "Paramètre manquant",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Veuillez d'abord vous connecter",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Échec de l'importation des utilisateurs"
},
"util": {
"No application is found for userId: %s": "Aucune application n'a été trouvée pour l'identifiant d'utilisateur : %s",
"No provider for category: %s is found for application: %s": "Aucun fournisseur pour la catégorie: %s n'est trouvé pour l'application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Gagal mengimpor pengguna",
"Missing parameter": "Parameter hilang",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Silahkan login terlebih dahulu",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Sandi baru tidak boleh mengandung spasi kosong.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Gagal mengimpor pengguna"
},
"util": {
"No application is found for userId: %s": "Tidak ditemukan aplikasi untuk userId: %s",
"No provider for category: %s is found for application: %s": "Tidak ditemukan penyedia untuk kategori: %s untuk aplikasi: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "ユーザーのインポートに失敗しました",
"Missing parameter": "不足しているパラメーター",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "最初にログインしてください",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "新しいパスワードにはスペースを含めることはできません。",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "ユーザーのインポートに失敗しました"
},
"util": {
"No application is found for userId: %s": "ユーザーIDに対するアプリケーションが見つかりません %s",
"No provider for category: %s is found for application: %s": "アプリケーション:%sのカテゴリ%sのプロバイダが見つかりません",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "사용자 가져오기를 실패했습니다",
"Missing parameter": "누락된 매개변수",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "먼저 로그인 하십시오",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "새 비밀번호에는 공백이 포함될 수 없습니다.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "사용자 가져오기를 실패했습니다"
},
"util": {
"No application is found for userId: %s": "어플리케이션을 찾을 수 없습니다. userId: %s",
"No provider for category: %s is found for application: %s": "어플리케이션 %s에서 %s 카테고리를 위한 공급자가 찾을 수 없습니다",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Falha ao importar usuários",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Falha ao importar usuários"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Не удалось импортировать пользователей",
"Missing parameter": "Отсутствующий параметр",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Пожалуйста, сначала войдите в систему",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Новый пароль не может содержать пробелы.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Не удалось импортировать пользователей"
},
"util": {
"No application is found for userId: %s": "Не найдено заявки для пользователя с идентификатором: %s",
"No provider for category: %s is found for application: %s": "Нет провайдера для категории: %s для приложения: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Nepodarilo sa importovať používateľov",
"Missing parameter": "Chýbajúci parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Najskôr sa prosím prihláste",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Nové heslo nemôže obsahovať medzery.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Nepodarilo sa importovať používateľov"
},
"util": {
"No application is found for userId: %s": "Nebola nájdená žiadna aplikácia pre userId: %s",
"No provider for category: %s is found for application: %s": "Pre aplikáciu: %s nebol nájdený žiadny poskytovateľ pre kategóriu: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Yeni şifreniz boşluk karakteri içeremez.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Không thể nhập người dùng",
"Missing parameter": "Thiếu tham số",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Vui lòng đăng nhập trước",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Không thể nhập người dùng"
},
"util": {
"No application is found for userId: %s": "Không tìm thấy ứng dụng cho ID người dùng: %s",
"No provider for category: %s is found for application: %s": "Không tìm thấy nhà cung cấp cho danh mục: %s cho ứng dụng: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "适配器: %s 未找到"
},
"general": {
"Failed to import groups": "导入群组失败",
"Failed to import users": "导入用户失败",
"Missing parameter": "缺少参数",
"Only admin user can specify user": "仅管理员用户可以指定用户",
"Please login first": "请先登录",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "新密码不可以包含空格",
"the user's owner and name should not be empty": "用户的组织和名称不能为空"
},
"user_upload": {
"Failed to import users": "导入用户失败"
},
"util": {
"No application is found for userId: %s": "未找到用户: %s的应用",
"No provider for category: %s is found for application: %s": "未找到类别为: %s的提供商来满足应用: %s",

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

@ -45,6 +45,7 @@ func main() {
object.InitUserManager()
object.InitFromFile()
object.InitCasvisorConfig()
object.InitCleanupTokens()
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
@ -63,6 +64,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
beego.InsertFilter("*", beego.BeforeRouter, routers.FieldValidationFilter)
beego.InsertFilter("*", beego.AfterExec, routers.AfterRecordMessage, false)
beego.BConfig.WebConfig.Session.SessionOn = true

View File

@ -252,12 +252,12 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
credManager := cred.GetCredManager(passwordType)
if credManager != nil {
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)
}
}
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)
}
@ -593,31 +593,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

@ -181,6 +181,41 @@ func AddGroups(groups []*Group) (bool, error) {
return affected != 0, nil
}
func AddGroupsInBatch(groups []*Group) (bool, error) {
if len(groups) == 0 {
return false, nil
}
session := ormer.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return false, err
}
for _, group := range groups {
err = checkGroupName(group.Name)
if err != nil {
return false, err
}
affected, err := session.Insert(group)
if err != nil {
return false, err
}
if affected == 0 {
return false, nil
}
}
err = session.Commit()
if err != nil {
return false, err
}
return true, nil
}
func deleteGroup(group *Group) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil {

61
object/group_upload.go Normal file
View File

@ -0,0 +1,61 @@
// 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 object
import (
"github.com/casdoor/casdoor/xlsx"
)
func getGroupMap(owner string) (map[string]*Group, error) {
m := map[string]*Group{}
groups, err := GetGroups(owner)
if err != nil {
return m, err
}
for _, group := range groups {
m[group.GetId()] = group
}
return m, nil
}
func UploadGroups(owner string, path string) (bool, error) {
table := xlsx.ReadXlsxFile(path)
oldGroupMap, err := getGroupMap(owner)
if err != nil {
return false, err
}
transGroups, err := StringArrayToStruct[Group](table)
if err != nil {
return false, err
}
newGroups := []*Group{}
for _, group := range transGroups {
if _, ok := oldGroupMap[group.GetId()]; !ok {
newGroups = append(newGroups, group)
}
}
if len(newGroups) == 0 {
return false, nil
}
return AddGroupsInBatch(newGroups)
}

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

@ -260,15 +260,15 @@ func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
res := make([]LdapUser, len(users))
for i, user := range users {
res[i] = LdapUser{
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
Uuid: user.GetLdapUuid(),
DisplayName: user.DisplayName,
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
RegisteredAddress: util.ReturnAnyNotEmpty(user.PostalAddress, user.RegisteredAddress),
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
Uuid: user.GetLdapUuid(),
DisplayName: user.DisplayName,
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
Address: util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
}
}
return res

View File

@ -222,7 +222,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 +536,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

@ -42,6 +42,7 @@ type Product struct {
IsRecharge bool `json:"isRecharge"`
Providers []string `xorm:"varchar(255)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
SuccessUrl string `xorm:"varchar(1000)" json:"successUrl"`
State string `xorm:"varchar(100)" json:"state"`
@ -213,6 +214,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
}
if product.SuccessUrl != "" {
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", product.SuccessUrl, owner, paymentName)
}
// Create an order
payReq := &pp.PayReq{
ProviderName: providerName,

93
object/token_cleanup.go Normal file
View File

@ -0,0 +1,93 @@
// 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 object
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/robfig/cron/v3"
)
func CleanupTokens(tokenRetentionIntervalAfterExpiry int) error {
var sessions []*Token
err := ormer.Engine.Find(&sessions)
if err != nil {
return fmt.Errorf("failed to query expired tokens: %w", err)
}
currentTime := time.Now()
deletedCount := 0
for _, session := range sessions {
tokenString := session.AccessToken
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
fmt.Printf("Failed to parse token %s: %v\n", session.Name, err)
continue
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
exp, ok := claims["exp"].(float64)
if !ok {
fmt.Printf("Token %s does not have an 'exp' claim\n", session.Name)
continue
}
expireTime := time.Unix(int64(exp), 0)
tokenAfterExpiry := currentTime.Sub(expireTime).Seconds()
if tokenAfterExpiry > float64(tokenRetentionIntervalAfterExpiry) {
_, err = ormer.Engine.Delete(session)
if err != nil {
return fmt.Errorf("failed to delete expired token %s: %w", session.Name, err)
}
fmt.Printf("[%d] Deleted expired token: %s | Created: %s | Org: %s | App: %s | User: %s\n",
deletedCount, session.Name, session.CreatedTime, session.Organization, session.Application, session.User)
deletedCount++
}
} else {
fmt.Printf("Token %s is not valid\n", session.Name)
}
}
return nil
}
func getTokenRetentionInterval(days int) int {
if days <= 0 {
days = 30
}
return days * 24 * 3600
}
func InitCleanupTokens() {
schedule := "0 0 * * *"
interval := getTokenRetentionInterval(30)
if err := CleanupTokens(interval); err != nil {
fmt.Printf("Error cleaning up tokens at startup: %v\n", err)
}
cronJob := cron.New()
_, err := cronJob.AddFunc(schedule, func() {
if err := CleanupTokens(interval); err != nil {
fmt.Printf("Error cleaning up tokens: %v\n", err)
}
})
if err != nil {
fmt.Printf("Error scheduling token cleanup: %v\n", err)
return
}
cronJob.Start()
}

View File

@ -212,6 +212,7 @@ type User struct {
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
}
@ -661,6 +662,62 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
return user, nil
}
func GetFilteredUser(user *User, isAdmin bool, isAdminOrSelf bool, accountItems []*AccountItem) (*User, error) {
if accountItems == nil || len(accountItems) == 0 {
return user, nil
}
userFieldMap := map[string]int{}
reflectedUserField := reflect.TypeOf(User{})
for i := 0; i < reflectedUserField.NumField(); i++ {
userFieldMap[strings.ToLower(reflectedUserField.Field(i).Name)] = i
}
reflectedUser := reflect.ValueOf(user).Elem()
for _, accountItem := range accountItems {
if accountItem.ViewRule == "Public" {
continue
} else if accountItem.ViewRule == "Self" && isAdminOrSelf {
continue
} else if accountItem.ViewRule == "Admin" && isAdmin {
continue
}
lowerCaseAccountItemName := strings.ToLower(accountItem.Name)
lowerCaseAccountItemName = strings.ReplaceAll(lowerCaseAccountItemName, " ", "")
switch accountItem.Name {
case "Multi-factor authentication":
lowerCaseAccountItemName = strings.ToLower("PreferredMfaType")
case "User type":
lowerCaseAccountItemName = "type"
case "Country/Region":
lowerCaseAccountItemName = "region"
case "ID card info":
{
infoKeys := []string{"idCardWithPerson", "idCardFront", "idCardWithPerson"}
for _, infoKey := range infoKeys {
if _, ok := user.Properties[infoKey]; ok {
user.Properties[infoKey] = ""
}
}
continue
}
}
fieldIdx, ok := userFieldMap[lowerCaseAccountItemName]
if !ok {
continue
}
reflectedUser.Field(fieldIdx).SetZero()
}
return user, nil
}
func GetMaskedUsers(users []*User, errs ...error) ([]*User, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
@ -739,7 +796,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
}
}
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

@ -81,7 +81,7 @@ func UploadUsers(owner string, path string) (bool, error) {
return false, err
}
transUsers, err := StringArrayToUser(table)
transUsers, err := StringArrayToStruct[User](table)
if err != nil {
return false, err
}

View File

@ -263,7 +263,19 @@ func ClearUserOAuthProperties(user *User, providerType string) (bool, error) {
return affected != 0, nil
}
func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang string) (bool, string) {
func userVisible(isAdmin bool, item *AccountItem) bool {
if item == nil {
return false
}
if item.ViewRule == "Admin" && !isAdmin {
return false
}
return true
}
func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, allowDisplayNameEmpty bool, lang string) (bool, string) {
organization, err := GetOrganizationByUser(oldUser)
if err != nil {
return false, err.Error()
@ -273,7 +285,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Owner != newUser.Owner {
item := GetAccountItemByName("Organization", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Owner = oldUser.Owner
} else {
itemsChanged = append(itemsChanged, item)
@ -281,7 +293,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Name != newUser.Name {
item := GetAccountItemByName("Name", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Name = oldUser.Name
} else {
itemsChanged = append(itemsChanged, item)
@ -289,7 +301,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Id != newUser.Id {
item := GetAccountItemByName("ID", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Id = oldUser.Id
} else {
itemsChanged = append(itemsChanged, item)
@ -297,15 +309,19 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.DisplayName != newUser.DisplayName {
item := GetAccountItemByName("Display name", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.DisplayName = oldUser.DisplayName
} else {
if !allowDisplayNameEmpty && newUser.DisplayName == "" {
return false, i18n.Translate(lang, "user:Display name cannot be empty")
}
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Avatar != newUser.Avatar {
item := GetAccountItemByName("Avatar", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Avatar = oldUser.Avatar
} else {
itemsChanged = append(itemsChanged, item)
@ -313,7 +329,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Type != newUser.Type {
item := GetAccountItemByName("User type", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Type = oldUser.Type
} else {
itemsChanged = append(itemsChanged, item)
@ -322,7 +338,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
// The password is *** when not modified
if oldUser.Password != newUser.Password && newUser.Password != "***" {
item := GetAccountItemByName("Password", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Password = oldUser.Password
} else {
itemsChanged = append(itemsChanged, item)
@ -330,7 +346,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Email != newUser.Email {
item := GetAccountItemByName("Email", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Email = oldUser.Email
} else {
itemsChanged = append(itemsChanged, item)
@ -338,7 +354,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Phone != newUser.Phone {
item := GetAccountItemByName("Phone", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Phone = oldUser.Phone
} else {
itemsChanged = append(itemsChanged, item)
@ -346,7 +362,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.CountryCode != newUser.CountryCode {
item := GetAccountItemByName("Country code", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.CountryCode = oldUser.CountryCode
} else {
itemsChanged = append(itemsChanged, item)
@ -354,7 +370,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Region != newUser.Region {
item := GetAccountItemByName("Country/Region", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Region = oldUser.Region
} else {
itemsChanged = append(itemsChanged, item)
@ -362,7 +378,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Location != newUser.Location {
item := GetAccountItemByName("Location", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Location = oldUser.Location
} else {
itemsChanged = append(itemsChanged, item)
@ -370,7 +386,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Affiliation != newUser.Affiliation {
item := GetAccountItemByName("Affiliation", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Affiliation = oldUser.Affiliation
} else {
itemsChanged = append(itemsChanged, item)
@ -378,7 +394,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Title != newUser.Title {
item := GetAccountItemByName("Title", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Title = oldUser.Title
} else {
itemsChanged = append(itemsChanged, item)
@ -386,7 +402,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Homepage != newUser.Homepage {
item := GetAccountItemByName("Homepage", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Homepage = oldUser.Homepage
} else {
itemsChanged = append(itemsChanged, item)
@ -394,7 +410,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Bio != newUser.Bio {
item := GetAccountItemByName("Bio", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Bio = oldUser.Bio
} else {
itemsChanged = append(itemsChanged, item)
@ -402,7 +418,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.Tag != newUser.Tag {
item := GetAccountItemByName("Tag", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Tag = oldUser.Tag
} else {
itemsChanged = append(itemsChanged, item)
@ -410,7 +426,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.SignupApplication != newUser.SignupApplication {
item := GetAccountItemByName("Signup application", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.SignupApplication = oldUser.SignupApplication
} else {
itemsChanged = append(itemsChanged, item)
@ -419,7 +435,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Gender != newUser.Gender {
item := GetAccountItemByName("Gender", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Gender = oldUser.Gender
} else {
itemsChanged = append(itemsChanged, item)
@ -428,7 +444,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Birthday != newUser.Birthday {
item := GetAccountItemByName("Birthday", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Birthday = oldUser.Birthday
} else {
itemsChanged = append(itemsChanged, item)
@ -437,7 +453,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Education != newUser.Education {
item := GetAccountItemByName("Education", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Education = oldUser.Education
} else {
itemsChanged = append(itemsChanged, item)
@ -446,7 +462,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.IdCard != newUser.IdCard {
item := GetAccountItemByName("ID card", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.IdCard = oldUser.IdCard
} else {
itemsChanged = append(itemsChanged, item)
@ -455,7 +471,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.IdCardType != newUser.IdCardType {
item := GetAccountItemByName("ID card type", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.IdCardType = oldUser.IdCardType
} else {
itemsChanged = append(itemsChanged, item)
@ -463,10 +479,13 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
oldUserPropertiesJson, _ := json.Marshal(oldUser.Properties)
if newUser.Properties == nil {
newUser.Properties = make(map[string]string)
}
newUserPropertiesJson, _ := json.Marshal(newUser.Properties)
if string(oldUserPropertiesJson) != string(newUserPropertiesJson) {
item := GetAccountItemByName("Properties", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Properties = oldUser.Properties
} else {
itemsChanged = append(itemsChanged, item)
@ -475,7 +494,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.PreferredMfaType != newUser.PreferredMfaType {
item := GetAccountItemByName("Multi-factor authentication", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.PreferredMfaType = oldUser.PreferredMfaType
} else {
itemsChanged = append(itemsChanged, item)
@ -486,13 +505,14 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
oldUser.Groups = []string{}
}
oldUserGroupsJson, _ := json.Marshal(oldUser.Groups)
if newUser.Groups == nil {
newUser.Groups = []string{}
}
newUserGroupsJson, _ := json.Marshal(newUser.Groups)
if string(oldUserGroupsJson) != string(newUserGroupsJson) {
item := GetAccountItemByName("Groups", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Groups = oldUser.Groups
} else {
itemsChanged = append(itemsChanged, item)
@ -510,7 +530,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
newUserAddressJson, _ := json.Marshal(newUser.Address)
if string(oldUserAddressJson) != string(newUserAddressJson) {
item := GetAccountItemByName("Address", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Address = oldUser.Address
} else {
itemsChanged = append(itemsChanged, item)
@ -519,7 +539,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if newUser.FaceIds != nil {
item := GetAccountItemByName("Face ID", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.FaceIds = oldUser.FaceIds
} else {
itemsChanged = append(itemsChanged, item)
@ -528,7 +548,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.IsAdmin != newUser.IsAdmin {
item := GetAccountItemByName("Is admin", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.IsAdmin = oldUser.IsAdmin
} else {
itemsChanged = append(itemsChanged, item)
@ -537,7 +557,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.IsForbidden != newUser.IsForbidden {
item := GetAccountItemByName("Is forbidden", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.IsForbidden = oldUser.IsForbidden
} else {
itemsChanged = append(itemsChanged, item)
@ -545,7 +565,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.IsDeleted != newUser.IsDeleted {
item := GetAccountItemByName("Is deleted", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.IsDeleted = oldUser.IsDeleted
} else {
itemsChanged = append(itemsChanged, item)
@ -553,7 +573,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.NeedUpdatePassword != newUser.NeedUpdatePassword {
item := GetAccountItemByName("Need update password", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.NeedUpdatePassword = oldUser.NeedUpdatePassword
} else {
itemsChanged = append(itemsChanged, item)
@ -561,7 +581,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
}
if oldUser.IpWhitelist != newUser.IpWhitelist {
item := GetAccountItemByName("IP whitelist", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.IpWhitelist = oldUser.IpWhitelist
} else {
itemsChanged = append(itemsChanged, item)
@ -570,7 +590,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Balance != newUser.Balance {
item := GetAccountItemByName("Balance", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Balance = oldUser.Balance
} else {
itemsChanged = append(itemsChanged, item)
@ -579,7 +599,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Score != newUser.Score {
item := GetAccountItemByName("Score", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Score = oldUser.Score
} else {
itemsChanged = append(itemsChanged, item)
@ -588,7 +608,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Karma != newUser.Karma {
item := GetAccountItemByName("Karma", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Karma = oldUser.Karma
} else {
itemsChanged = append(itemsChanged, item)
@ -597,7 +617,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Language != newUser.Language {
item := GetAccountItemByName("Language", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Language = oldUser.Language
} else {
itemsChanged = append(itemsChanged, item)
@ -606,7 +626,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Ranking != newUser.Ranking {
item := GetAccountItemByName("Ranking", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Ranking = oldUser.Ranking
} else {
itemsChanged = append(itemsChanged, item)
@ -615,7 +635,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Currency != newUser.Currency {
item := GetAccountItemByName("Currency", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Currency = oldUser.Currency
} else {
itemsChanged = append(itemsChanged, item)
@ -624,7 +644,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Hash != newUser.Hash {
item := GetAccountItemByName("Hash", organization)
if item == nil {
if !userVisible(isAdmin, item) {
newUser.Hash = oldUser.Hash
} else {
itemsChanged = append(itemsChanged, item)
@ -704,14 +724,14 @@ func setReflectAttr[T any](fieldValue *reflect.Value, fieldString string) error
return nil
}
func StringArrayToUser(stringArray [][]string) ([]*User, error) {
func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
fieldNames := stringArray[0]
excelMap := []map[string]string{}
userFieldMap := map[string]int{}
structFieldMap := map[string]int{}
reflectedUser := reflect.TypeOf(User{})
for i := 0; i < reflectedUser.NumField(); i++ {
userFieldMap[strings.ToLower(reflectedUser.Field(i).Name)] = i
reflectedStruct := reflect.TypeOf(*new(T))
for i := 0; i < reflectedStruct.NumField(); i++ {
structFieldMap[strings.ToLower(reflectedStruct.Field(i).Name)] = i
}
for idx, field := range stringArray {
@ -726,22 +746,23 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) {
excelMap = append(excelMap, tempMap)
}
users := []*User{}
instances := []*T{}
var err error
for _, u := range excelMap {
user := User{}
reflectedUser := reflect.ValueOf(&user).Elem()
for k, v := range u {
for _, m := range excelMap {
instance := new(T)
reflectedInstance := reflect.ValueOf(instance).Elem()
for k, v := range m {
if v == "" || v == "null" || v == "[]" || v == "{}" {
continue
}
fName := strings.ToLower(strings.ReplaceAll(k, "_", ""))
fieldIdx, ok := userFieldMap[fName]
fieldIdx, ok := structFieldMap[fName]
if !ok {
continue
}
fv := reflectedUser.Field(fieldIdx)
fv := reflectedInstance.Field(fieldIdx)
if !fv.IsValid() {
continue
}
@ -786,8 +807,8 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) {
return nil, err
}
}
users = append(users, &user)
instances = append(instances, instance)
}
return users, nil
return instances, nil
}

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)

View File

@ -0,0 +1,56 @@
// 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 routers
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/beego/beego/context"
)
var forbiddenChars = `/?:#&%=+;`
func FieldValidationFilter(ctx *context.Context) {
if ctx.Input.Method() != "POST" {
return
}
urlPath := ctx.Request.URL.Path
if !(strings.HasPrefix(urlPath, "/api/add-") || strings.HasPrefix(urlPath, "/api/update-")) {
return
}
bodyBytes, err := io.ReadAll(ctx.Request.Body)
if err != nil || len(bodyBytes) == 0 {
return
}
ctx.Request.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
var requestData map[string]interface{}
if err := json.Unmarshal(bodyBytes, &requestData); err != nil {
return
}
if value, ok := requestData["name"].(string); ok {
if strings.ContainsAny(value, forbiddenChars) {
responseError(ctx, fmt.Sprintf("Field 'name' contains forbidden characters: %q", forbiddenChars))
return
}
}
}

View File

@ -81,6 +81,7 @@ func initAPI() {
beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup")
beego.Router("/api/upload-groups", &controllers.ApiController{}, "POST:UploadGroups")
beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")

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

@ -30,6 +30,7 @@ import (
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/process"
)
type SystemInfo struct {
@ -60,7 +61,17 @@ func getMemoryUsage() (uint64, uint64, error) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.Alloc, virtualMem.Total, nil
proc, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
return 0, 0, err
}
memInfo, err := proc.MemoryInfo()
if err != nil {
return 0, 0, err
}
return memInfo.RSS, virtualMem.Total, nil
}
func GetSystemInfo() (*SystemInfo, error) {

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Space, Switch, Upload, message} from "antd";
import {CopyOutlined, HolderOutlined, LinkOutlined, UploadOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend";
@ -279,6 +279,13 @@ class ApplicationEditPage extends React.Component {
</Col>
<Col span={22} >
<Input value={this.state.application.name} disabled={this.state.application.name === "app-built-in"} onChange={e => {
const value = e.target.value;
if (/[/?:@#&%=+;]/.test(value)) {
const invalidChars = "/ ? : @ # & % = + ;";
const messageText = i18next.t("application:Invalid characters in application name") + ":" + " " + invalidChars;
message.error(messageText);
return;
}
this.updateApplicationField("name", e.target.value);
}} />
</Col>

View File

@ -14,7 +14,8 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Table, Tooltip} from "antd";
import {Button, Table, Tooltip, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import moment from "moment";
import * as Setting from "./Setting";
import * as GroupBackend from "./backend/GroupBackend";
@ -87,6 +88,42 @@ class GroupListPage extends BaseListPage {
});
}
uploadFile(info) {
const {status, response: res} = info.file;
if (status === "done") {
if (res.status === "ok") {
Setting.showMessage("success", "Groups uploaded successfully, refreshing the page");
const {pagination} = this.state;
this.fetch({pagination});
} else {
Setting.showMessage("error", `Groups failed to upload: ${res.msg}`);
}
} else if (status === "error") {
Setting.showMessage("error", "File failed to upload");
}
}
renderUpload() {
const props = {
name: "file",
accept: ".xlsx",
method: "post",
action: `${Setting.ServerUrl}/api/upload-groups`,
withCredentials: true,
onChange: (info) => {
this.uploadFile(info);
},
};
return (
<Upload {...props}>
<Button icon={<UploadOutlined />} id="upload-button" type="primary" size="small">
{i18next.t("group:Upload (.xlsx)")}
</Button>
</Upload>
);
}
renderTable(data) {
const columns = [
{
@ -231,7 +268,10 @@ class GroupListPage extends BaseListPage {
title={() => (
<div>
{i18next.t("general:Groups")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
{
this.renderUpload()
}
</div>
)}
loading={this.state.loading}

View File

@ -288,6 +288,16 @@ class ProductEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Success URL"), i18next.t("product:Success URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.product.successUrl} onChange={e => {
this.updateProductField("successUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :

View File

@ -371,11 +371,6 @@ class ProviderEditPage extends React.Component {
{id: "Third-party", name: i18next.t("provider:Third-party")},
]
);
} else if (type === "Aliyun Captcha") {
return [
{id: "nc", name: i18next.t("provider:Sliding Validation")},
{id: "ic", name: i18next.t("provider:Intelligent Validation")},
];
} else {
return [];
}
@ -674,7 +669,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
{
this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" && this.state.provider.type !== "Aliyun Captcha" ? null : (
this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>

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

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

@ -166,7 +166,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,7 +176,7 @@ 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;
@ -185,7 +185,7 @@ class AuthCallback extends React.Component {
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 +207,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

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Col, Form, Input, Row, Select, Steps} from "antd";
import {Button, Col, Form, Input, Popover, Row, Select, Steps} from "antd";
import * as AuthBackend from "./AuthBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Util from "./Util";
@ -385,30 +385,48 @@ class ForgetPage extends React.Component {
},
]}
/>
<Form.Item
name="newPassword"
hidden={this.state.current !== 2}
rules={[
{
required: true,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
<Popover placement="right" content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Form.Item
name="newPassword"
hidden={this.state.current !== 2}
rules={[
{
required: true,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
},
},
},
]}
hasFeedback
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
</Form.Item>
]}
hasFeedback
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
onChange={(e) => {
this.setState({
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, e.target.value),
});
}}
onFocus={() => {
this.setState({
passwordPopoverOpen: true,
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("newPassword") ?? ""),
});
}}
onBlur={() => {
this.setState({
passwordPopoverOpen: false,
});
}}
/>
</Form.Item>
</Popover>
<Form.Item
name="confirm"
dependencies={["newPassword"]}

View File

@ -134,6 +134,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;
}
@ -443,6 +445,9 @@ class LoginPage extends React.Component {
} else if (captchaRule === CaptchaRule.Dynamic) {
this.checkCaptchaStatus(values);
return;
} else if (captchaRule === CaptchaRule.InternetOnly) {
this.checkCaptchaStatus(values);
return;
}
}
this.login(values);
@ -491,9 +496,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 +510,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 +522,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({
@ -961,9 +966,23 @@ class LoginPage extends React.Component {
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}

View File

@ -278,7 +278,7 @@ const authInfo = {
endpoint: "https://www.tiktok.com/auth/authorize/",
},
Tumblr: {
scope: "email",
scope: "basic",
endpoint: "https://www.tumblr.com/oauth2/authorize",
},
Twitch: {

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Form, Input, Radio, Result, Row, Select, message} from "antd";
import {Button, Form, Input, Popover, Radio, Result, Row, Select, message} from "antd";
import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend";
import * as ProviderButton from "./ProviderButton";
@ -607,28 +607,45 @@ class SignupPage extends React.Component {
}
} else if (signupItem.name === "Password") {
return (
<Form.Item
name="password"
className="signup-password"
label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
rules={[
{
required: required,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
<Popover placement="right" content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Form.Item
name="password"
className="signup-password"
label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
rules={[
{
required: required,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
},
},
},
]}
hasFeedback
>
<Input.Password className="signup-password-input" placeholder={signupItem.placeholder} />
</Form.Item>
]}
hasFeedback
>
<Input.Password className="signup-password-input" placeholder={signupItem.placeholder} onChange={(e) => {
this.setState({
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, e.target.value),
});
}}
onFocus={() => {
this.setState({
passwordPopoverOpen: true,
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("password") ?? ""),
});
}}
onBlur={() => {
this.setState({
passwordPopoverOpen: false,
});
}} />
</Form.Item>
</Popover>
);
} else if (signupItem.name === "Confirm password") {
return (

View File

@ -1,4 +1,4 @@
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {CopyOutlined} from "@ant-design/icons";
import {Button, Col, Form, Input, QRCode, Space} from "antd";
import copy from "copy-to-clipboard";
import i18next from "i18next";
@ -47,11 +47,11 @@ export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
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>

View File

@ -85,22 +85,31 @@ export const CaptchaWidget = (props) => {
break;
}
case "Aliyun Captcha": {
window.AliyunCaptchaConfig = {
region: "cn",
prefix: clientSecret2,
};
const AWSCTimer = setInterval(() => {
if (!window.AWSC) {
loadScript("https://g.alicdn.com/AWSC/AWSC/awsc.js");
if (!window.initAliyunCaptcha) {
loadScript("https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js");
}
if (window.AWSC) {
if (window.initAliyunCaptcha) {
if (clientSecret2 && clientSecret2 !== "***") {
window.AWSC.use(subType, function(state, module) {
module.init({
appkey: clientSecret2,
scene: clientId2,
renderTo: "captcha",
success: function(data) {
onChange(`SessionId=${data.sessionId}&AccessKeyId=${siteKey}&Scene=${clientId2}&AppKey=${clientSecret2}&Token=${data.token}&Sig=${data.sig}&RemoteIp=192.168.0.1`);
},
});
window.initAliyunCaptcha({
SceneId: clientId2,
mode: "embed",
element: "#captcha",
captchaVerifyCallback: (data) => {
onChange(data.toString());
},
slideStyle: {
width: 320,
height: 40,
},
language: "cn",
immediate: true,
});
}
clearInterval(AWSCTimer);

View File

@ -13,6 +13,8 @@
// limitations under the License.
import i18next from "i18next";
import React from "react";
import {CheckCircleTwoTone, CloseCircleTwoTone} from "@ant-design/icons";
function isValidOption_AtLeast6(password) {
if (password.length < 6) {
@ -52,6 +54,33 @@ function isValidOption_NoRepeat(password) {
return "";
}
const checkers = {
AtLeast6: isValidOption_AtLeast6,
AtLeast8: isValidOption_AtLeast8,
Aa123: isValidOption_Aa123,
SpecialChar: isValidOption_SpecialChar,
NoRepeat: isValidOption_NoRepeat,
};
function getOptionDescription(option, password) {
switch (option) {
case "AtLeast6": return i18next.t("user:The password must have at least 6 characters");
case "AtLeast8": return i18next.t("user:The password must have at least 8 characters");
case "Aa123": return i18next.t("user:The password must contain at least one uppercase letter, one lowercase letter and one digit");
case "SpecialChar": return i18next.t("user:The password must contain at least one special character");
case "NoRepeat": return i18next.t("user:The password must not contain any repeated characters");
}
}
export function renderPasswordPopover(options, password) {
return <div style={{width: 240}} >
{options.map((option, idx) => {
return <div key={idx}>{checkers[option](password) === "" ? <CheckCircleTwoTone twoToneColor={"#52c41a"} /> :
<CloseCircleTwoTone twoToneColor={"#ff4d4f"} />} {getOptionDescription(option, password)}</div>;
})}
</div>;
}
export function checkPasswordComplexity(password, options) {
if (password.length === 0) {
return i18next.t("login:Please input your password!");
@ -61,14 +90,6 @@ export function checkPasswordComplexity(password, options) {
return "";
}
const checkers = {
AtLeast6: isValidOption_AtLeast6,
AtLeast8: isValidOption_AtLeast8,
Aa123: isValidOption_Aa123,
SpecialChar: isValidOption_SpecialChar,
NoRepeat: isValidOption_NoRepeat,
};
for (const option of options) {
const checkerFunc = checkers[option];
if (checkerFunc) {

View File

@ -44,6 +44,12 @@ export const CaptchaModal = (props) => {
}
}, [visible]);
useEffect(() => {
if (captchaToken !== "" && captchaType !== "Default") {
handleOk();
}
}, [captchaToken]);
const handleOk = () => {
onOk?.(captchaType, captchaToken, clientSecret);
};
@ -138,19 +144,18 @@ export const CaptchaModal = (props) => {
if (!regex.test(captchaToken)) {
isOkDisabled = true;
}
} else if (captchaToken === "") {
isOkDisabled = true;
return [
null,
<Button key="ok" disabled={isOkDisabled} type="primary" onClick={handleOk}>{i18next.t("general:OK")}</Button>,
];
}
return [
<Button key="cancel" onClick={handleCancel}>{i18next.t("general:Cancel")}</Button>,
<Button key="ok" disabled={isOkDisabled} type="primary" onClick={handleOk}>{i18next.t("general:OK")}</Button>,
];
return null;
};
return (
<Modal
closable={false}
closable={true}
maskClosable={false}
destroyOnClose={true}
title={i18next.t("general:Captcha")}
@ -160,6 +165,7 @@ export const CaptchaModal = (props) => {
width={350}
footer={renderFooter()}
onCancel={handleCancel}
afterClose={handleCancel}
onOk={handleOk}
>
<div style={{marginTop: "20px", marginBottom: "50px"}}>
@ -175,4 +181,5 @@ export const CaptchaRule = {
Always: "Always",
Never: "Never",
Dynamic: "Dynamic",
InternetOnly: "Internet-Only",
};

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Input, Modal, Row} from "antd";
import {Button, Col, Input, Modal, Popover, Row} from "antd";
import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
@ -35,6 +35,8 @@ export const PasswordModal = (props) => {
const [rePasswordValid, setRePasswordValid] = React.useState(false);
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = React.useState("");
const [rePasswordErrorMessage, setRePasswordErrorMessage] = React.useState("");
const [passwordPopoverOpen, setPasswordPopoverOpen] = React.useState(false);
const [passwordPopover, setPasswordPopover] = React.useState();
React.useEffect(() => {
if (organization) {
@ -130,12 +132,26 @@ export const PasswordModal = (props) => {
</Row>
) : null}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password
addonBefore={i18next.t("user:New Password")}
placeholder={i18next.t("user:input password")}
onChange={(e) => {handleNewPassword(e.target.value);}}
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
/>
<Popover placement="right" 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);
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, e.target.value));
}}
onFocus={() => {
setPasswordPopoverOpen(true);
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, newPassword));
}}
onBlur={() => {
setPasswordPopoverOpen(false);
}}
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
/>
</Popover>
</Row>
{!newPasswordValid && newPasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{newPasswordErrorMessage}</div>}
<Row style={{width: "100%", marginBottom: "20px"}}>

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"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",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Sold",
"Sold - Tooltip": "Quantity sold",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Tag of product",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Name of host",
"IdP": "IdP",
"IdP certificate": "IdP certificate",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL",
@ -946,7 +950,6 @@
"Silent": "Silent",
"Site key": "Site key",
"Site key - Tooltip": "Site key",
"Sliding Validation": "Sliding Validation",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Přizpůsobit hlavičku vstupní stránky vaší aplikace",
"Incremental": "Inkrementální",
"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",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Nadřazená skupina - Tooltip",
"Physical": "Fyzická",
"Show all": "Zobrazit vše",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtuální",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Prodáno",
"Sold - Tooltip": "Prodávané množství",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Štítek produktu",
"Test buy page..": "Testovací stránka nákupu..",
"There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Název hostitele",
"IdP": "IdP",
"IdP certificate": "Certifikát IdP",
"Intelligent Validation": "Inteligentní validace",
"Internal": "Interní",
"Issuer URL": "URL vydavatele",
"Issuer URL - Tooltip": "URL vydavatele",
@ -946,7 +950,6 @@
"Silent": "Tiché",
"Site key": "Klíč stránky",
"Site key - Tooltip": "Nápověda ke klíči stránky",
"Sliding Validation": "Posuvné ověření",
"Sub type": "Podtyp",
"Sub type - Tooltip": "Nápověda k podtypu",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"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",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Verkauft",
"Sold - Tooltip": "Menge verkauft",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Tag des Produkts",
"Test buy page..": "Testkaufseite.",
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Name des Hosts",
"IdP": "IdP",
"IdP certificate": "IdP-Zertifikat",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Issuer URL": "Issuer-URL",
"Issuer URL - Tooltip": "Emittenten-URL",
@ -946,7 +950,6 @@
"Silent": "Silent",
"Site key": "Site-Key",
"Site key - Tooltip": "Seitenschlüssel",
"Sliding Validation": "Sliding Validation",
"Sub type": "Untertyp",
"Sub type - Tooltip": "Unterart",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"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",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Sold",
"Sold - Tooltip": "Quantity sold",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Tag of product",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Name of host",
"IdP": "IdP",
"IdP certificate": "IdP certificate",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL",
@ -946,7 +950,6 @@
"Silent": "Silent",
"Site key": "Site key",
"Site key - Tooltip": "Site key",
"Sliding Validation": "Sliding Validation",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"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",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Vendido",
"Sold - Tooltip": "Cantidad vendida",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Etiqueta de producto",
"Test buy page..": "Página de compra de prueba.",
"There is no payment channel for this product.": "No hay canal de pago para este producto.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Nombre del anfitrión",
"IdP": "IdP = Proveedor de Identidad",
"IdP certificate": "Certificado de proveedor de identidad (IdP)",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Issuer URL": "URL del emisor",
"Issuer URL - Tooltip": "URL del emisor",
@ -946,7 +950,6 @@
"Silent": "Silent",
"Site key": "Clave del sitio",
"Site key - Tooltip": "Clave del sitio",
"Sliding Validation": "Sliding Validation",
"Sub type": "Subtipo",
"Sub type - Tooltip": "Subtipo",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "کد head صفحه ورود برنامه خود را سفارشی کنید",
"Incremental": "افزایشی",
"Input": "ورودی",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "کد دعوت",
"Left": "چپ",
"Logged in successfully": "با موفقیت وارد شدید",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "گروه والد - راهنمای ابزار",
"Physical": "فیزیکی",
"Show all": "نمایش همه",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "مجازی",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "فروخته شده",
"Sold - Tooltip": "تعداد فروخته شده",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "برچسب محصول",
"Test buy page..": "صفحه تست خرید..",
"There is no payment channel for this product.": "برای این محصول کانال پرداختی وجود ندارد.",
@ -860,7 +865,6 @@
"Host - Tooltip": "نام میزبان",
"IdP": "IdP",
"IdP certificate": "گواهی IdP",
"Intelligent Validation": "اعتبارسنجی هوشمند",
"Internal": "داخلی",
"Issuer URL": "آدرس صادرکننده",
"Issuer URL - Tooltip": "آدرس صادرکننده",
@ -946,7 +950,6 @@
"Silent": "بی‌صدا",
"Site key": "کلید سایت",
"Site key - Tooltip": "کلید سایت",
"Sliding Validation": "اعتبارسنجی کشویی",
"Sub type": "زیرنوع",
"Sub type - Tooltip": "زیرنوع",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"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",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Sold",
"Sold - Tooltip": "Quantity sold",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Tag of product",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Name of host",
"IdP": "IdP",
"IdP certificate": "IdP certificate",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL",
@ -946,7 +950,6 @@
"Silent": "Silent",
"Site key": "Site key",
"Site key - Tooltip": "Site key",
"Sliding Validation": "Sliding Validation",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incrémentale",
"Input": "Saisie",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Code d'invitation",
"Left": "Gauche",
"Logged in successfully": "Connexion réussie",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Groupe parent - Infobulle",
"Physical": "Physique",
"Show all": "Tout afficher",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtuel",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Vendu",
"Sold - Tooltip": "Quantité vendue",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Étiquette de produit",
"Test buy page..": "Page d'achat de test.",
"There is no payment channel for this product.": "Il n'y a aucun canal de paiement pour ce produit.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Nom d'hôte",
"IdP": "IdP (Identité Fournisseur)",
"IdP certificate": "Certificat IdP",
"Intelligent Validation": "Validation intelligente",
"Internal": "Interne",
"Issuer URL": "URL de l'émetteur",
"Issuer URL - Tooltip": "URL de l'émetteur",
@ -946,7 +950,6 @@
"Silent": "Silencieux",
"Site key": "Clé de site",
"Site key - Tooltip": "Clé de site",
"Sliding Validation": "Validation glissante",
"Sub type": "Sous-type",
"Sub type - Tooltip": "Sous-type",
"Subject": "Subject",

View File

@ -76,6 +76,8 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental",
"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",
@ -454,6 +456,7 @@
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Upload (.xlsx)": "Upload (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
},
@ -778,6 +781,8 @@
"Sold": "Sold",
"Sold - Tooltip": "Quantity sold",
"Stripe": "Stripe",
"Success URL": "Success URL",
"Success URL - Tooltip": "URL to return to after purchase",
"Tag - Tooltip": "Tag of product",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
@ -860,7 +865,6 @@
"Host - Tooltip": "Name of host",
"IdP": "IdP",
"IdP certificate": "IdP certificate",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL",
@ -946,7 +950,6 @@
"Silent": "Silent",
"Site key": "Site key",
"Site key - Tooltip": "Site key",
"Sliding Validation": "Sliding Validation",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type",
"Subject": "Subject",

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