Compare commits

...

35 Commits

Author SHA1 Message Date
2da597b26f feat: add support for per-account MFA validity period in org setting to reduce repeated prompts (#3917) 2025-07-11 00:24:33 +08:00
ef14c84edc feat: show the popover on the top when window's width too small and close popover when password options is empty (#3952) 2025-07-10 19:56:05 +08:00
cb5c7667b5 feat: change Subscription's StartTime and EndTime to string 2025-07-10 14:11:40 +08:00
920ed87f75 fix: refactor the code in CheckPassword() 2025-07-10 00:49:13 +08:00
6598f0ccdf feat: use token's client ID instead in IntrospectToken() API (#3948) 2025-07-09 22:07:44 +08:00
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
142 changed files with 2442 additions and 1566 deletions

View File

@ -15,32 +15,51 @@
package captcha package captcha
import ( import (
"encoding/json" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"errors" openapiutil "github.com/alibabacloud-go/openapi-util/service"
"fmt" teaUtil "github.com/alibabacloud-go/tea-utils/v2/service"
"io" "github.com/alibabacloud-go/tea/tea"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/casdoor/casdoor/util"
) )
const AliyunCaptchaVerifyUrl = "http://afs.aliyuncs.com" const AliyunCaptchaVerifyUrl = "captcha.cn-shanghai.aliyuncs.com"
type captchaSuccessResponse struct { type VerifyCaptchaRequest struct {
Code int `json:"Code"` CaptchaVerifyParam *string `json:"CaptchaVerifyParam,omitempty" xml:"CaptchaVerifyParam,omitempty"`
Msg string `json:"Msg"` SceneId *string `json:"SceneId,omitempty" xml:"SceneId,omitempty"`
} }
type captchaFailResponse struct { type VerifyCaptchaResponseBodyResult struct {
Code string `json:"Code"` VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"`
Message string `json:"Message"`
} }
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{} type AliyunCaptchaProvider struct{}
func NewAliyunCaptchaProvider() *AliyunCaptchaProvider { func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
@ -48,68 +67,69 @@ func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
return captcha return captcha
} }
func contentEscape(str string) string { func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
str = strings.Replace(str, " ", "%20", -1) config := &openapi.Config{}
str = url.QueryEscape(str)
return str
}
func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { config.Endpoint = tea.String(AliyunCaptchaVerifyUrl)
pathData, err := url.ParseQuery(token) 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 { if err != nil {
return false, err return false, err
} }
pathData["Action"] = []string{"AuthenticateSig"} request := VerifyCaptchaRequest{CaptchaVerifyParam: tea.String(token), SceneId: tea.String(clientId2)}
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"}
var keys []string err = teaUtil.ValidateModel(&request)
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)))
if err != nil { if err != nil {
return false, err return false, err
} }
defer resp.Body.Close() runtime := &teaUtil.RuntimeOptions{}
body, err := io.ReadAll(resp.Body)
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 { if err != nil {
return false, err return false, err
} }
return handleCaptchaResponse(body) err = tea.Convert(resBody, &res)
}
func handleCaptchaResponse(body []byte) (bool, error) {
captchaResp := &captchaSuccessResponse{}
err := json.Unmarshal(body, captchaResp)
if err != nil { if err != nil {
captchaFailResp := &captchaFailResponse{} return false, err
err = json.Unmarshal(body, captchaFailResp)
if err != nil {
return false, err
}
return false, errors.New(captchaFailResp.Message)
} }
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 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 return object.VerifyCaptcha(clientSecret, token), nil
} }

View File

@ -35,7 +35,7 @@ func NewGEETESTCaptchaProvider() *GEETESTCaptchaProvider {
return captcha 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) pathData, err := url.ParseQuery(token)
if err != nil { if err != nil {
return false, err return false, err

View File

@ -32,7 +32,7 @@ func NewHCaptchaProvider() *HCaptchaProvider {
return captcha 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{ reqData := url.Values{
"secret": {clientSecret}, "secret": {clientSecret},
"response": {token}, "response": {token},

View File

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

View File

@ -32,7 +32,7 @@ func NewCloudflareTurnstileProvider() *CloudflareTurnstileProvider {
return captcha 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{ reqData := url.Values{
"secret": {clientSecret}, "secret": {clientSecret},
"response": {token}, "response": {token},

View File

@ -66,7 +66,11 @@ func GetConfigBool(key string) bool {
func GetConfigInt64(key string) (int64, error) { func GetConfigInt64(key string) (int64, error) {
value := GetConfigString(key) value := GetConfigString(key)
num, err := strconv.ParseInt(value, 10, 64) 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 { func GetConfigDataSourceName() string {

View File

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

View File

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

View File

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

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

View File

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

View File

@ -49,6 +49,6 @@ func (c *ApiController) UploadPermissions() {
if affected { if affected {
c.ResponseOk() c.ResponseOk()
} else { } 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") paidUserName := c.Input().Get("userName")
owner, _ := util.GetOwnerAndNameFromId(id) owner, _ := util.GetOwnerAndNameFromId(id)
userId := util.GetId(owner, paidUserName) 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")) c.ResponseError(c.T("general:Only admin user can specify user"))
return return
} }

View File

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

View File

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

View File

@ -197,8 +197,8 @@ func (c *ApiController) GetUser() {
return return
} }
var organization *object.Organization
if user != nil { if user != nil {
var organization *object.Organization
organization, err = object.GetOrganizationByUser(user) organization, err = object.GetOrganizationByUser(user)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
@ -237,6 +237,14 @@ func (c *ApiController) GetUser() {
return 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) c.ResponseOk(user)
} }
@ -282,13 +290,6 @@ func (c *ApiController) UpdateUser() {
return 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 == "" { if user.MfaEmailEnabled && user.Email == "" {
c.ResponseError(c.T("user:MFA email is enabled but email is empty")) c.ResponseError(c.T("user:MFA email is enabled but email is empty"))
return return
@ -310,7 +311,8 @@ func (c *ApiController) UpdateUser() {
} }
isAdmin := c.IsAdmin() 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) c.ResponseError(err)
return return
} }
@ -572,7 +574,7 @@ func (c *ApiController) SetPassword() {
targetUser.LastChangePasswordTime = util.GetCurrentTime() targetUser.LastChangePasswordTime = util.GetCurrentTime()
if user.Ldap == "" { if user.Ldap == "" {
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false) _, err = object.UpdateUser(userId, targetUser, []string{"password", "password_salt", "need_update_password", "password_type", "last_change_password_time"}, false)
} else { } else {
if isAdmin { if isAdmin {
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage()) err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())

View File

@ -67,6 +67,6 @@ func (c *ApiController) UploadUsers() {
if affected { if affected {
c.ResponseOk() c.ResponseOk()
} else { } 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 { if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType) c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return 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()) c.ResponseError(err.Error())
return return
} else if !isHuman { } else if !isHuman {
@ -349,7 +349,7 @@ func (c *ApiController) VerifyCaptcha() {
return return
} }
isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret) isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2 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 v1.3.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/aws/aws-sdk-go v1.45.5 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/darabonba-number v1.0.4 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // 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/openplatform-20191219/v2 v2.0.1 // indirect
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // 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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "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", "Missing parameter": "Chybějící parametr",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Prosím, přihlaste se nejprve", "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.", "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" "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": { "util": {
"No application is found for userId: %s": "Pro userId: %s nebyla nalezena žádná aplikace", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Fehler beim Importieren von Benutzern",
"Missing parameter": "Fehlender Parameter", "Missing parameter": "Fehlender Parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Bitte zuerst einloggen", "Please login first": "Bitte zuerst einloggen",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.", "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" "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": { "util": {
"No application is found for userId: %s": "Es wurde keine Anwendung für die Benutzer-ID gefunden: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Error al importar usuarios",
"Missing parameter": "Parámetro faltante", "Missing parameter": "Parámetro faltante",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Por favor, inicia sesión primero", "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.", "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" "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": { "util": {
"No application is found for userId: %s": "No se encuentra ninguna aplicación para el Id de usuario: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "عدم موفقیت در وارد کردن کاربران",
"Missing parameter": "پارامتر گمشده", "Missing parameter": "پارامتر گمشده",
"Only admin user can specify user": "Only admin user can specify user", "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" "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": { "util": {
"No application is found for userId: %s": "هیچ برنامه‌ای برای userId: %s یافت نشد", "No application is found for userId: %s": "هیچ برنامه‌ای برای userId: %s یافت نشد",
"No provider for category: %s is found for application: %s": "هیچ ارائه‌دهنده‌ای برای دسته‌بندی: %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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Échec de l'importation des utilisateurs",
"Missing parameter": "Paramètre manquant", "Missing parameter": "Paramètre manquant",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Veuillez d'abord vous connecter", "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.", "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" "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": { "util": {
"No application is found for userId: %s": "Aucune application n'a été trouvée pour l'identifiant d'utilisateur : %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Gagal mengimpor pengguna",
"Missing parameter": "Parameter hilang", "Missing parameter": "Parameter hilang",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Silahkan login terlebih dahulu", "Please login first": "Silahkan login terlebih dahulu",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Sandi baru tidak boleh mengandung spasi kosong.", "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" "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": { "util": {
"No application is found for userId: %s": "Tidak ditemukan aplikasi untuk userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "ユーザーのインポートに失敗しました",
"Missing parameter": "不足しているパラメーター", "Missing parameter": "不足しているパラメーター",
"Only admin user can specify user": "Only admin user can specify user", "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" "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": { "util": {
"No application is found for userId: %s": "ユーザーIDに対するアプリケーションが見つかりません %s", "No application is found for userId: %s": "ユーザーIDに対するアプリケーションが見つかりません %s",
"No provider for category: %s is found for application: %s": "アプリケーション:%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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "사용자 가져오기를 실패했습니다",
"Missing parameter": "누락된 매개변수", "Missing parameter": "누락된 매개변수",
"Only admin user can specify user": "Only admin user can specify user", "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" "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": { "util": {
"No application is found for userId: %s": "어플리케이션을 찾을 수 없습니다. userId: %s", "No application is found for userId: %s": "어플리케이션을 찾을 수 없습니다. userId: %s",
"No provider for category: %s is found for application: %s": "어플리케이션 %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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Falha ao importar usuários",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Не удалось импортировать пользователей",
"Missing parameter": "Отсутствующий параметр", "Missing parameter": "Отсутствующий параметр",
"Only admin user can specify user": "Only admin user can specify user", "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" "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": { "util": {
"No application is found for userId: %s": "Не найдено заявки для пользователя с идентификатором: %s", "No application is found for userId: %s": "Не найдено заявки для пользователя с идентификатором: %s",
"No provider for category: %s is found for application: %s": "Нет провайдера для категории: %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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "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", "Missing parameter": "Chýbajúci parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Najskôr sa prosím prihláste", "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.", "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" "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": { "util": {
"No application is found for userId: %s": "Nebola nájdená žiadna aplikácia pre userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Yeni şifreniz boşluk karakteri içeremez.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter", "Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first", "Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.", "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" "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": { "util": {
"No application is found for userId: %s": "No application is found for userId: %s", "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", "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" "the adapter: %s is not found": "the adapter: %s is not found"
}, },
"general": { "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ố", "Missing parameter": "Thiếu tham số",
"Only admin user can specify user": "Only admin user can specify user", "Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Vui lòng đăng nhập trước", "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.", "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" "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": { "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 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", "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 未找到" "the adapter: %s is not found": "适配器: %s 未找到"
}, },
"general": { "general": {
"Failed to import groups": "导入群组失败",
"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": "导入用户失败"
},
"util": { "util": {
"No application is found for userId: %s": "未找到用户: %s的应用", "No application is found for userId: %s": "未找到用户: %s的应用",
"No provider for category: %s is found for application: %s": "未找到类别为: %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{ userInfo := UserInfo{
Id: douyinUserInfo.Data.OpenId, Id: douyinUserInfo.Data.OpenId,
Username: douyinUserInfo.Data.Nickname, Username: douyinUserInfo.Data.OpenId,
DisplayName: douyinUserInfo.Data.Nickname, DisplayName: douyinUserInfo.Data.Nickname,
AvatarUrl: douyinUserInfo.Data.Avatar, AvatarUrl: douyinUserInfo.Data.Avatar,
} }

View File

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

View File

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

View File

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

View File

@ -181,6 +181,41 @@ func AddGroups(groups []*Group) (bool, error) {
return affected != 0, nil 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) { func deleteGroup(group *Group) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{}) affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil { 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" import "testing"
func TestDumpToFile(t *testing.T) { func TestDumpToFile(t *testing.T) {
createDatabase = false
InitConfig() InitConfig()
err := DumpToFile("./init_data_dump.json") err := DumpToFile("./init_data_dump.json")

View File

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

View File

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

View File

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

View File

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

View File

@ -42,6 +42,7 @@ type Product struct {
IsRecharge bool `json:"isRecharge"` IsRecharge bool `json:"isRecharge"`
Providers []string `xorm:"varchar(255)" json:"providers"` Providers []string `xorm:"varchar(255)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
SuccessUrl string `xorm:"varchar(1000)" json:"successUrl"`
State string `xorm:"varchar(100)" json:"state"` State string `xorm:"varchar(100)" json:"state"`
@ -205,14 +206,24 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if plan == nil { if plan == nil {
return nil, nil, fmt.Errorf("the plan: %s does not exist", planName) return nil, nil, fmt.Errorf("the plan: %s does not exist", planName)
} }
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
if err != nil {
return nil, nil, err
}
_, err = AddSubscription(sub) _, err = AddSubscription(sub)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name) 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 // Create an order
payReq := &pp.PayReq{ payReq := &pp.PayReq{
ProviderName: providerName, ProviderName: providerName,

View File

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

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

@ -210,10 +210,12 @@ type User struct {
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"` SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"` MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
NeedUpdatePassword bool `json:"needUpdatePassword"` MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"` MfaRememberDeadline string `xorm:"varchar(100)" json:"mfaRememberDeadline"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
} }
type Userinfo struct { type Userinfo struct {
@ -661,6 +663,62 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
return user, nil 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) { func GetMaskedUsers(users []*User, errs ...error) ([]*User, error) {
if len(errs) > 0 && errs[0] != nil { if len(errs) > 0 && errs[0] != nil {
return nil, errs[0] return nil, errs[0]
@ -735,11 +793,11 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup", "eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo", "spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_items", "mfa_remember_deadline",
} }
} }
if isAdmin { if isAdmin {
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance") columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "mfa_items")
} }
columns = append(columns, "updated_time") columns = append(columns, "updated_time")

View File

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

View File

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

View File

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

View File

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

View File

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

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/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup") beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup") 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-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers") 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/go-git/go-git/v5/plumbing/object"
"github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem" "github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/process"
) )
type SystemInfo struct { type SystemInfo struct {
@ -60,7 +61,17 @@ func getMemoryUsage() (uint64, uint64, error) {
var m runtime.MemStats var m runtime.MemStats
runtime.ReadMemStats(&m) runtime.ReadMemStats(&m)
return m.Sys, 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) { func GetSystemInfo() (*SystemInfo, error) {

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; 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 {CopyOutlined, HolderOutlined, LinkOutlined, UploadOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
@ -279,6 +279,13 @@ class ApplicationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.application.name} disabled={this.state.application.name === "app-built-in"} onChange={e => { <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); this.updateApplicationField("name", e.target.value);
}} /> }} />
</Col> </Col>

View File

@ -14,7 +14,8 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; 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 moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as GroupBackend from "./backend/GroupBackend"; 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) { renderTable(data) {
const columns = [ const columns = [
{ {
@ -231,7 +268,10 @@ class GroupListPage extends BaseListPage {
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Groups")}&nbsp;&nbsp;&nbsp;&nbsp; {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> </div>
)} )}
loading={this.state.loading} loading={this.state.loading}

View File

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

View File

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

View File

@ -288,6 +288,16 @@ class ProductEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} : {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")}, {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 { } else {
return []; return [];
} }
@ -674,7 +669,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </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> <React.Fragment>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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