Compare commits

...

22 Commits

Author SHA1 Message Date
3a19d4c7c8 fix: do not filter webhooks by org 2024-02-06 20:33:11 +08:00
a60be2b2ab feat: refactor MFA code and fix no-session bug (#2676)
* refactor: refactor mfa

* refactor: refactor mfa

* refactor: refactor mfa

* lint

* chore: reduce wait time
2024-02-06 20:17:59 +08:00
06ef97a080 feat: can delete the whole SigninMethodTable 2024-02-06 16:43:16 +08:00
167c1b0f1b feat: fix bug in WeChat OA login (#2674)
* fix: fix the problem of Wechat Official Account login

* fix: fix code format problem

* fix: add error display and fix the code format problem

* fix: i18n problem and code format
2024-02-05 21:38:12 +08:00
7d0eae230e fix: fix /signup organization parameter issue (#2669) 2024-02-03 11:47:36 +08:00
901867e8bb feat: fix /signup parameter issue 2024-02-03 10:00:47 +08:00
b7be1943fa feat: Add Invitation Code to Generate Invitation Link (#2666)
Add auto-population of invitation fields in the registration page based on the invitation code in the link
2024-02-02 21:12:56 +08:00
bbbda1982f feat: fix missing MFA session issue (#2667) 2024-02-02 10:23:17 +08:00
e593f5be5b fix: improve code format (#2665)
* feat: replace io/ioutils pacakage with io/os package

* fix: add missing error handling
2024-02-01 23:06:12 +08:00
0918757e85 feat: add template support for Custom HTTP SMS provider (#2662) 2024-02-01 17:50:22 +08:00
ce0d45a70b feat: support SAML POST binding (#2661)
* fix: support saml http post binding

* fix: support saml http post binding

* fix: support saml post binding sp
2024-02-01 17:28:56 +08:00
c4096788b2 feat: ABAC support for /api/enforce endpoint (#2660) 2024-01-31 23:14:55 +08:00
523186f895 feat: Support sha512 password encryption algorithm (#2657)
* add sha512 encryption support for password

* fead: add sha512 encryption support for password
2024-01-31 00:06:06 +08:00
ef373ca736 feat: add deletedTime to user (#2652) 2024-01-30 23:18:32 +08:00
721a681ff1 fix: improve error handling in GetUserApplication() 2024-01-30 21:40:39 +08:00
8b1c4b0c75 feat: make phone field longer to 100 2024-01-30 19:06:18 +08:00
540f22f8bd feat: refactor GetTokenByTokenValue() 2024-01-29 10:03:33 +08:00
79f81f1356 Improve error handling in IntrospectToken() 2024-01-29 09:58:40 +08:00
4e145f71b5 feat: improve MFA UI and jump URL (#2647)
* fix: mfa UI

* fix: mfa UI
2024-01-28 16:46:35 +08:00
104f975a2f fix: fix wrong org issue for user's "signupApplication" 2024-01-28 01:51:03 +08:00
71bb400559 feat: support using org's defaultAvatar when adding user in web UI 2024-01-28 01:07:20 +08:00
93c3c78d42 feat: support "id_card" in UpdateUser() 2024-01-26 08:23:55 +08:00
107 changed files with 1016 additions and 367 deletions

View File

@ -51,7 +51,8 @@ p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, * p, *, *, GET, /api/userinfo, *, *
p, *, *, GET, /api/user, *, * p, *, *, GET, /api/user, *, *
p, *, *, GET, /api/health, *, * p, *, *, GET, /api/health, *, *
p, *, *, POST, /api/webhook, *, * p, *, *, *, /api/webhook, *, *
p, *, *, GET, /api/get-qrcode, *, *
p, *, *, GET, /api/get-webhook-event, *, * p, *, *, GET, /api/get-webhook-event, *, *
p, *, *, GET, /api/get-captcha-status, *, * p, *, *, GET, /api/get-captcha-status, *, *
p, *, *, *, /api/login/oauth, *, * p, *, *, *, /api/login/oauth, *, *
@ -80,6 +81,7 @@ p, *, *, *, /.well-known/jwks, *, *
p, *, *, GET, /api/get-saml-login, *, * p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, * p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, * p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /api/saml/redirect, *, *
p, *, *, *, /cas, *, * p, *, *, *, /cas, *, *
p, *, *, *, /scim, *, * p, *, *, *, /scim, *, *
p, *, *, *, /api/webauthn, *, * p, *, *, *, /api/webauthn, *, *
@ -95,6 +97,7 @@ p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, * p, *, *, GET, /api/get-all-objects, *, *
p, *, *, GET, /api/get-all-actions, *, * p, *, *, GET, /api/get-all-actions, *, *
p, *, *, GET, /api/get-all-roles, *, * p, *, *, GET, /api/get-all-roles, *, *
p, *, *, GET, /api/get-invitation-info, *, *
` `
sa := stringadapter.NewAdapter(ruleText) sa := stringadapter.NewAdapter(ruleText)
@ -150,7 +153,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool { func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" { if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") { if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" {
return true return true
} else if urlPath == "/api/update-user" { } else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information // Allow ordinary users to update their own information

View File

@ -93,6 +93,10 @@ func (c *ApiController) Signup() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist")), authForm.Application)
return
}
if !application.EnableSignUp { if !application.EnableSignUp {
c.ResponseError(c.T("account:The application does not allow to sign up new account")) c.ResponseError(c.T("account:The application does not allow to sign up new account"))
@ -105,6 +109,11 @@ func (c *ApiController) Signup() {
return return
} }
if organization == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s does not exist")), authForm.Organization)
return
}
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage()) msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
if msg != "" { if msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
@ -227,7 +236,7 @@ func (c *ApiController) Signup() {
if invitation != nil { if invitation != nil {
invitation.UsedCount += 1 invitation.UsedCount += 1
_, err := object.UpdateInvitation(invitation.GetId(), invitation) _, err := object.UpdateInvitation(invitation.GetId(), invitation, c.GetAcceptLanguage())
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -139,6 +139,10 @@ func (c *ApiController) GetUserApplication() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The organization: %s should have one application at least"), user.Owner))
return
}
c.ResponseOk(object.GetMaskedApplication(application, userId)) c.ResponseOk(object.GetMaskedApplication(application, userId))
} }

View File

@ -19,7 +19,7 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -38,7 +38,7 @@ import (
) )
var ( var (
wechatScanType string wechatCacheMap map[string]idp.WechatCacheMapValue
lock sync.RWMutex lock sync.RWMutex
) )
@ -912,56 +912,122 @@ func (c *ApiController) HandleSamlLogin() {
samlResponse = url.QueryEscape(samlResponse) samlResponse = url.QueryEscape(samlResponse)
targetUrl := fmt.Sprintf("%s?relayState=%s&samlResponse=%s", targetUrl := fmt.Sprintf("%s?relayState=%s&samlResponse=%s",
slice[4], relayState, samlResponse) slice[4], relayState, samlResponse)
c.Redirect(targetUrl, 303) c.Redirect(targetUrl, http.StatusSeeOther)
} }
// HandleOfficialAccountEvent ... // HandleOfficialAccountEvent ...
// @Tag System API // @Tag System API
// @Title HandleOfficialAccountEvent // @Title HandleOfficialAccountEvent
// @router /webhook [POST] // @router /webhook [POST]
// @Success 200 {object} object.Userinfo The Response object // @Success 200 {object} controllers.Response The Response object
func (c *ApiController) HandleOfficialAccountEvent() { func (c *ApiController) HandleOfficialAccountEvent() {
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body) if c.Ctx.Request.Method == "GET" {
s := c.Ctx.Request.FormValue("echostr")
echostr, _ := strconv.Atoi(s)
c.SetData(echostr)
c.ServeJSON()
return
}
respBytes, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
signature := c.Input().Get("signature")
timestamp := c.Input().Get("timestamp")
nonce := c.Input().Get("nonce")
var data struct { var data struct {
MsgType string `xml:"MsgType"` MsgType string `xml:"MsgType"`
Event string `xml:"Event"` Event string `xml:"Event"`
EventKey string `xml:"EventKey"` EventKey string `xml:"EventKey"`
FromUserName string `xml:"FromUserName"`
Ticket string `xml:"Ticket"`
} }
err = xml.Unmarshal(respBytes, &data) err = xml.Unmarshal(respBytes, &data)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if strings.ToUpper(data.Event) != "SCAN" && strings.ToUpper(data.Event) != "SUBSCRIBE" {
c.Ctx.WriteString("")
return
}
if data.Ticket == "" {
c.ResponseError(err.Error())
return
}
providerId := data.EventKey
provider, err := object.GetProvider(providerId)
if err != nil {
c.ResponseError(err.Error())
return
}
if data.Ticket == "" {
c.ResponseError("empty ticket")
return
}
if !idp.VerifyWechatSignature(provider.Content, nonce, timestamp, signature) {
c.ResponseError("invalid signature")
return
}
lock.Lock() lock.Lock()
defer lock.Unlock() if wechatCacheMap == nil {
if data.EventKey != "" { wechatCacheMap = make(map[string]idp.WechatCacheMapValue)
wechatScanType = data.Event
c.Ctx.WriteString("")
} }
wechatCacheMap[data.Ticket] = idp.WechatCacheMapValue{
IsScanned: true,
WechatOpenId: data.FromUserName,
}
lock.Unlock()
c.Ctx.WriteString("")
} }
// GetWebhookEventType ... // GetWebhookEventType ...
// @Tag System API // @Tag System API
// @Title GetWebhookEventType // @Title GetWebhookEventType
// @router /get-webhook-event [GET] // @router /get-webhook-event [GET]
// @Success 200 {object} object.Userinfo The Response object // @Param ticket query string true "The eventId of QRCode"
// @Success 200 {object} controllers.Response The Response object
func (c *ApiController) GetWebhookEventType() { func (c *ApiController) GetWebhookEventType() {
lock.Lock() ticket := c.Input().Get("ticket")
defer lock.Unlock()
resp := &Response{ lock.RLock()
Status: "ok", wechatMsg, ok := wechatCacheMap[ticket]
Msg: "", lock.RUnlock()
Data: wechatScanType, if !ok {
c.ResponseError("ticket not found")
return
} }
c.Data["json"] = resp lock.Lock()
wechatScanType = "" delete(wechatCacheMap, ticket)
c.ServeJSON() lock.Unlock()
c.ResponseOk("SCAN", wechatMsg.IsScanned)
}
// GetQRCode
// @Tag System API
// @Title GetWechatQRCode
// @router /get-qrcode [GET]
// @Param id query string true "The id ( owner/name ) of provider"
// @Success 200 {object} controllers.Response The Response object
func (c *ApiController) GetQRCode() {
providerId := c.Input().Get("id")
provider, err := object.GetProvider(providerId)
if err != nil {
c.ResponseError(err.Error())
return
}
code, ticket, err := idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2, providerId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(code, ticket)
} }
// GetCaptchaStatus // GetCaptchaStatus

View File

@ -121,6 +121,10 @@ func (c *ApiController) Enforce() {
} }
} else if owner != "" { } else if owner != "" {
permissions, err = object.GetPermissions(owner) permissions, err = object.GetPermissions(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
} else { } else {
c.ResponseError(c.T("general:Missing parameter")) c.ResponseError(c.T("general:Missing parameter"))
return return
@ -235,6 +239,10 @@ func (c *ApiController) BatchEnforce() {
} }
} else if owner != "" { } else if owner != "" {
permissions, err = object.GetPermissions(owner) permissions, err = object.GetPermissions(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
} else { } else {
c.ResponseError(c.T("general:Missing parameter")) c.ResponseError(c.T("general:Missing parameter"))
return return

View File

@ -84,6 +84,32 @@ func (c *ApiController) GetInvitation() {
c.ResponseOk(invitation) c.ResponseOk(invitation)
} }
// GetInvitationCodeInfo
// @Title GetInvitationCodeInfo
// @Tag Invitation API
// @Description get invitation code information
// @Param code query string true "Invitation code"
// @Success 200 {object} object.Invitation The Response object
// @router /get-invitation-info [get]
func (c *ApiController) GetInvitationCodeInfo() {
code := c.Input().Get("code")
applicationId := c.Input().Get("applicationId")
application, err := object.GetApplication(applicationId)
if err != nil {
c.ResponseError(err.Error())
return
}
invitation, msg := object.GetInvitationByCode(code, application.Organization, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
return
}
c.ResponseOk(object.GetMaskedInvitation(invitation))
}
// UpdateInvitation // UpdateInvitation
// @Title UpdateInvitation // @Title UpdateInvitation
// @Tag Invitation API // @Tag Invitation API
@ -102,7 +128,7 @@ func (c *ApiController) UpdateInvitation() {
return return
} }
c.Data["json"] = wrapActionResponse(object.UpdateInvitation(id, &invitation)) c.Data["json"] = wrapActionResponse(object.UpdateInvitation(id, &invitation, c.GetAcceptLanguage()))
c.ServeJSON() c.ServeJSON()
} }
@ -121,7 +147,7 @@ func (c *ApiController) AddInvitation() {
return return
} }
c.Data["json"] = wrapActionResponse(object.AddInvitation(&invitation)) c.Data["json"] = wrapActionResponse(object.AddInvitation(&invitation, c.GetAcceptLanguage()))
c.ServeJSON() c.ServeJSON()
} }

View File

@ -19,6 +19,14 @@ import (
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/google/uuid"
)
const (
MfaRecoveryCodesSession = "mfa_recovery_codes"
MfaCountryCodeSession = "mfa_country_code"
MfaDestSession = "mfa_dest"
MfaTotpSecretSession = "mfa_totp_secret"
) )
// MfaSetupInitiate // MfaSetupInitiate
@ -57,12 +65,20 @@ func (c *ApiController) MfaSetupInitiate() {
return return
} }
mfaProps, err := MfaUtil.Initiate(c.Ctx, user.GetId()) mfaProps, err := MfaUtil.Initiate(user.GetId())
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
recoveryCode := uuid.NewString()
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
if mfaType == object.TotpType {
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
}
mfaProps.RecoveryCodes = []string{recoveryCode}
resp := mfaProps resp := mfaProps
c.ResponseOk(resp) c.ResponseOk(resp)
} }
@ -83,13 +99,39 @@ func (c *ApiController) MfaSetupVerify() {
c.ResponseError("missing auth type or passcode") c.ResponseError("missing auth type or passcode")
return return
} }
mfaUtil := object.GetMfaUtil(mfaType, nil)
config := &object.MfaProps{
MfaType: mfaType,
}
if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession)
if secret == nil {
c.ResponseError("totp secret is missing")
return
}
config.Secret = secret.(string)
} else if mfaType == object.EmailType || mfaType == object.SmsType {
dest := c.GetSession(MfaDestSession)
if dest == nil {
c.ResponseError("destination is missing")
return
}
config.Secret = dest.(string)
countryCode := c.GetSession(MfaCountryCodeSession)
if countryCode == nil {
c.ResponseError("country code is missing")
return
}
config.CountryCode = countryCode.(string)
}
mfaUtil := object.GetMfaUtil(mfaType, config)
if mfaUtil == nil { if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type") c.ResponseError("Invalid multi-factor authentication type")
return return
} }
err := mfaUtil.SetupVerify(c.Ctx, passcode) err := mfaUtil.SetupVerify(passcode)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} else { } else {
@ -122,18 +164,58 @@ func (c *ApiController) MfaSetupEnable() {
return return
} }
mfaUtil := object.GetMfaUtil(mfaType, nil) config := &object.MfaProps{
MfaType: mfaType,
}
if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession)
if secret == nil {
c.ResponseError("totp secret is missing")
return
}
config.Secret = secret.(string)
} else if mfaType == object.EmailType || mfaType == object.SmsType {
dest := c.GetSession(MfaDestSession)
if dest == nil {
c.ResponseError("destination is missing")
return
}
config.Secret = dest.(string)
countryCode := c.GetSession(MfaCountryCodeSession)
if countryCode == nil {
c.ResponseError("country code is missing")
return
}
config.CountryCode = countryCode.(string)
}
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
if recoveryCodes == nil {
c.ResponseError("recovery codes is missing")
return
}
config.RecoveryCodes = []string{recoveryCodes.(string)}
mfaUtil := object.GetMfaUtil(mfaType, config)
if mfaUtil == nil { if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type") c.ResponseError("Invalid multi-factor authentication type")
return return
} }
err = mfaUtil.Enable(c.Ctx, user) err = mfaUtil.Enable(user)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.DelSession(MfaRecoveryCodesSession)
if mfaType == object.TotpType {
c.DelSession(MfaTotpSecretSession)
} else {
c.DelSession(MfaCountryCodeSession)
c.DelSession(MfaDestSession)
}
c.ResponseOk(http.StatusText(http.StatusOK)) c.ResponseOk(http.StatusText(http.StatusOK))
} }

View File

@ -16,6 +16,7 @@ package controllers
import ( import (
"fmt" "fmt"
"net/http"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
) )
@ -34,7 +35,13 @@ func (c *ApiController) GetSamlMeta() {
return return
} }
metadata, err := object.GetSamlMeta(application, host) enablePostBinding, err := c.GetBool("enablePostBinding", false)
if err != nil {
c.ResponseError(err.Error())
return
}
metadata, err := object.GetSamlMeta(application, host, enablePostBinding)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -43,3 +50,17 @@ func (c *ApiController) GetSamlMeta() {
c.Data["xml"] = metadata c.Data["xml"] = metadata
c.ServeXML() c.ServeXML()
} }
func (c *ApiController) HandleSamlRedirect() {
host := c.Ctx.Request.Host
owner := c.Ctx.Input.Param(":owner")
application := c.Ctx.Input.Param(":application")
relayState := c.Input().Get("RelayState")
samlRequest := c.Input().Get("SAMLRequest")
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host)
c.Redirect(targetURL, http.StatusSeeOther)
}

View File

@ -47,6 +47,11 @@ func (c *ApiController) GetSystemInfo() {
// @router /get-version-info [get] // @router /get-version-info [get]
func (c *ApiController) GetVersionInfo() { func (c *ApiController) GetVersionInfo() {
versionInfo, err := util.GetVersionInfo() versionInfo, err := util.GetVersionInfo()
if err != nil {
c.ResponseError(err.Error())
return
}
if versionInfo.Version != "" { if versionInfo.Version != "" {
c.ResponseOk(versionInfo) c.ResponseOk(versionInfo)
return return

View File

@ -271,6 +271,14 @@ func (c *ApiController) RefreshToken() {
c.ServeJSON() c.ServeJSON()
} }
func (c *ApiController) ResponseTokenError(errorMsg string) {
c.Data["json"] = &object.TokenError{
Error: errorMsg,
}
c.SetTokenErrorHttpStatus()
c.ServeJSON()
}
// IntrospectToken // IntrospectToken
// @Title IntrospectToken // @Title IntrospectToken
// @Tag Login API // @Tag Login API
@ -293,40 +301,33 @@ func (c *ApiController) IntrospectToken() {
clientId = c.Input().Get("client_id") clientId = c.Input().Get("client_id")
clientSecret = c.Input().Get("client_secret") clientSecret = c.Input().Get("client_secret")
if clientId == "" || clientSecret == "" { if clientId == "" || clientSecret == "" {
c.ResponseError(c.T("token:Empty clientId or clientSecret")) c.ResponseTokenError(object.InvalidRequest)
c.Data["json"] = &object.TokenError{
Error: object.InvalidRequest,
}
c.SetTokenErrorHttpStatus()
c.ServeJSON()
return return
} }
} }
application, err := object.GetApplicationByClientId(clientId) application, err := object.GetApplicationByClientId(clientId)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseTokenError(err.Error())
return return
} }
if application == nil || application.ClientSecret != clientSecret { if application == nil || application.ClientSecret != clientSecret {
c.ResponseError(c.T("token:Invalid application or wrong clientSecret")) c.ResponseTokenError(c.T("token:Invalid application or wrong clientSecret"))
c.Data["json"] = &object.TokenError{
Error: object.InvalidClient,
}
c.SetTokenErrorHttpStatus()
return
}
token, err := object.GetTokenByTokenAndApplication(tokenValue, application.Name)
if err != nil {
c.ResponseError(err.Error())
return return
} }
token, err := object.GetTokenByTokenValue(tokenValue)
if err != nil {
c.ResponseTokenError(err.Error())
return
}
if token == nil { if token == nil {
c.Data["json"] = &object.IntrospectionResponse{Active: false} c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON() c.ServeJSON()
return return
} }
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application) jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil { if err != nil || jwtToken.Valid() != nil {
// and token revoked case. but we not implement // and token revoked case. but we not implement

View File

@ -161,7 +161,7 @@ func (c *ApiController) SendVerificationCode() {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }
} else if vform.Method == MfaSetupVerification { } else if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaDestSession, vform.Dest) c.SetSession(MfaDestSession, vform.Dest)
} }
provider, err := application.GetEmailProvider() provider, err := application.GetEmailProvider()
@ -198,8 +198,8 @@ func (c *ApiController) SendVerificationCode() {
} }
if vform.Method == MfaSetupVerification { if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaCountryCodeSession, vform.CountryCode) c.SetSession(MfaCountryCodeSession, vform.CountryCode)
c.SetSession(object.MfaDestSession, vform.Dest) c.SetSession(MfaDestSession, vform.Dest)
} }
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false) mfaProps := user.GetPreferredMfaProps(false)

View File

@ -24,6 +24,8 @@ func GetCredManager(passwordType string) CredManager {
return NewPlainCredManager() return NewPlainCredManager()
} else if passwordType == "salt" { } else if passwordType == "salt" {
return NewSha256SaltCredManager() return NewSha256SaltCredManager()
} else if passwordType == "sha512-salt" {
return NewSha512SaltCredManager()
} else if passwordType == "md5-salt" { } else if passwordType == "md5-salt" {
return NewMd5UserSaltCredManager() return NewMd5UserSaltCredManager()
} else if passwordType == "bcrypt" { } else if passwordType == "bcrypt" {

50
cred/sha512-salt.go Normal file
View File

@ -0,0 +1,50 @@
// Copyright 2024 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 cred
import (
"crypto/sha512"
"encoding/hex"
)
type Sha512SaltCredManager struct{}
func getSha512(data []byte) []byte {
hash := sha512.Sum512(data)
return hash[:]
}
func getSha512HexDigest(s string) string {
b := getSha512([]byte(s))
res := hex.EncodeToString(b)
return res
}
func NewSha512SaltCredManager() *Sha512SaltCredManager {
cm := &Sha512SaltCredManager{}
return cm
}
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
res := getSha512HexDigest(password)
if organizationSalt != "" {
res = getSha512HexDigest(res + organizationSalt)
}
return res
}
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
}

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein", "Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein", "DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
"DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname", "DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname",
"Email already exists": "E-Mail existiert bereits", "Email already exists": "E-Mail existiert bereits",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Afiliación no puede estar en blanco", "Affiliation cannot be blank": "Afiliación no puede estar en blanco",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco", "DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
"DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido", "DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido",
"Email already exists": "El correo electrónico ya existe", "Email already exists": "El correo electrónico ya existe",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation ne peut pas être vide", "Affiliation cannot be blank": "Affiliation ne peut pas être vide",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide", "DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
"DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide", "DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide",
"Email already exists": "E-mail déjà existant", "Email already exists": "E-mail déjà existant",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Keterkaitan tidak boleh kosong", "Affiliation cannot be blank": "Keterkaitan tidak boleh kosong",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong", "DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong",
"DisplayName is not valid real name": "DisplayName bukanlah nama asli yang valid", "DisplayName is not valid real name": "DisplayName bukanlah nama asli yang valid",
"Email already exists": "Email sudah ada", "Email already exists": "Email sudah ada",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "所属は空白にできません", "Affiliation cannot be blank": "所属は空白にできません",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "表示名は空白にできません", "DisplayName cannot be blank": "表示名は空白にできません",
"DisplayName is not valid real name": "表示名は有効な実名ではありません", "DisplayName is not valid real name": "表示名は有効な実名ではありません",
"Email already exists": "メールは既に存在します", "Email already exists": "メールは既に存在します",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "소속은 비워 둘 수 없습니다", "Affiliation cannot be blank": "소속은 비워 둘 수 없습니다",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다", "DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다",
"DisplayName is not valid real name": "DisplayName는 유효한 실제 이름이 아닙니다", "DisplayName is not valid real name": "DisplayName는 유효한 실제 이름이 아닙니다",
"Email already exists": "이메일이 이미 존재합니다", "Email already exists": "이메일이 이미 존재합니다",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Принадлежность не может быть пустым значением", "Affiliation cannot be blank": "Принадлежность не может быть пустым значением",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Имя отображения не может быть пустым", "DisplayName cannot be blank": "Имя отображения не может быть пустым",
"DisplayName is not valid real name": "DisplayName не является действительным именем", "DisplayName is not valid real name": "DisplayName не является действительным именем",
"Email already exists": "Электронная почта уже существует", "Email already exists": "Электронная почта уже существует",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Tình trạng liên kết không thể để trống", "Affiliation cannot be blank": "Tình trạng liên kết không thể để trống",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Tên hiển thị không thể để trống", "DisplayName cannot be blank": "Tên hiển thị không thể để trống",
"DisplayName is not valid real name": "DisplayName không phải là tên thật hợp lệ", "DisplayName is not valid real name": "DisplayName không phải là tên thật hợp lệ",
"Email already exists": "Email đã tồn tại", "Email already exists": "Email đã tồn tại",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "工作单位不可为空", "Affiliation cannot be blank": "工作单位不可为空",
"Default code does not match the code's matching rules": "邀请码默认值和邀请码规则不匹配",
"DisplayName cannot be blank": "显示名称不可为空", "DisplayName cannot be blank": "显示名称不可为空",
"DisplayName is not valid real name": "显示名称必须是真实姓名", "DisplayName is not valid real name": "显示名称必须是真实姓名",
"Email already exists": "该邮箱已存在", "Email already exists": "该邮箱已存在",

View File

@ -16,13 +16,15 @@ package idp
import ( import (
"bytes" "bytes"
"crypto/sha1"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"sort"
"strings" "strings"
"time" "time"
@ -35,6 +37,11 @@ type WeChatIdProvider struct {
Config *oauth2.Config Config *oauth2.Config
} }
type WechatCacheMapValue struct {
IsScanned bool
WechatOpenId string
}
func NewWeChatIdProvider(clientId string, clientSecret string, redirectUrl string) *WeChatIdProvider { func NewWeChatIdProvider(clientId string, clientSecret string, redirectUrl string) *WeChatIdProvider {
idp := &WeChatIdProvider{} idp := &WeChatIdProvider{}
@ -204,60 +211,70 @@ func BuildWechatOpenIdKey(appId string) string {
return fmt.Sprintf("wechat_openid_%s", appId) return fmt.Sprintf("wechat_openid_%s", appId)
} }
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, error) { func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, string, error) {
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret) accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
request, err := http.NewRequest("GET", accessTokenUrl, nil) request, err := http.NewRequest("GET", accessTokenUrl, nil)
if err != nil { if err != nil {
return "", err return "", "", err
} }
client := new(http.Client) client := new(http.Client)
resp, err := client.Do(request) resp, err := client.Do(request)
if err != nil { if err != nil {
return "", err return "", "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
respBytes, err := ioutil.ReadAll(resp.Body) respBytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", err return "", "", err
} }
var data struct { var data struct {
ExpireIn int `json:"expires_in"` ExpireIn int `json:"expires_in"`
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
ErrCode int `json:"errcode"`
Errmsg string `json:errmsg`
} }
err = json.Unmarshal(respBytes, &data) err = json.Unmarshal(respBytes, &data)
if err != nil { if err != nil {
return "", err return "", "", err
} }
return data.AccessToken, nil return data.AccessToken, data.Errmsg, nil
} }
func GetWechatOfficialAccountQRCode(clientId string, clientSecret string) (string, error) { func GetWechatOfficialAccountQRCode(clientId string, clientSecret string, providerId string) (string, string, error) {
accessToken, err := GetWechatOfficialAccountAccessToken(clientId, clientSecret) accessToken, errMsg, err := GetWechatOfficialAccountAccessToken(clientId, clientSecret)
if err != nil {
return "", "", err
}
if errMsg != "" {
return "", "", fmt.Errorf("Fail to fetch WeChat QRcode: %s", errMsg)
}
client := new(http.Client) client := new(http.Client)
weChatEndpoint := "https://api.weixin.qq.com/cgi-bin/qrcode/create" weChatEndpoint := "https://api.weixin.qq.com/cgi-bin/qrcode/create"
qrCodeUrl := fmt.Sprintf("%s?access_token=%s", weChatEndpoint, accessToken) qrCodeUrl := fmt.Sprintf("%s?access_token=%s", weChatEndpoint, accessToken)
params := `{"action_name": "QR_LIMIT_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}` params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
bodyData := bytes.NewReader([]byte(params)) bodyData := bytes.NewReader([]byte(params))
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData) requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
if err != nil { if err != nil {
return "", err return "", "", err
} }
resp, err := client.Do(requeset) resp, err := client.Do(requeset)
if err != nil { if err != nil {
return "", err return "", "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
respBytes, err := ioutil.ReadAll(resp.Body) respBytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", err return "", "", err
} }
var data struct { var data struct {
Ticket string `json:"ticket"` Ticket string `json:"ticket"`
@ -266,11 +283,26 @@ func GetWechatOfficialAccountQRCode(clientId string, clientSecret string) (strin
} }
err = json.Unmarshal(respBytes, &data) err = json.Unmarshal(respBytes, &data)
if err != nil { if err != nil {
return "", err return "", "", err
} }
var png []byte var png []byte
png, err = qrcode.Encode(data.URL, qrcode.Medium, 256) png, err = qrcode.Encode(data.URL, qrcode.Medium, 256)
base64Image := base64.StdEncoding.EncodeToString(png) base64Image := base64.StdEncoding.EncodeToString(png)
return base64Image, nil return base64Image, data.Ticket, nil
}
func VerifyWechatSignature(token string, nonce string, timestamp string, signature string) bool {
// verify the signature
tmpArr := sort.StringSlice{token, timestamp, nonce}
sort.Sort(tmpArr)
tmpStr := ""
for _, str := range tmpArr {
tmpStr = tmpStr + str
}
b := sha1.Sum([]byte(tmpStr))
res := hex.EncodeToString(b[:])
return res == signature
} }

View File

@ -37,7 +37,7 @@ type Adapter struct {
Host string `xorm:"varchar(100)" json:"host"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"` User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"` Password string `xorm:"varchar(150)" json:"password"`
Database string `xorm:"varchar(100)" json:"database"` Database string `xorm:"varchar(100)" json:"database"`
*xormadapter.Adapter `xorm:"-" json:"-"` *xormadapter.Adapter `xorm:"-" json:"-"`

View File

@ -19,7 +19,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
@ -52,31 +51,32 @@ type Application struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Logo string `xorm:"varchar(200)" json:"logo"` Logo string `xorm:"varchar(200)" json:"logo"`
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"` HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
Description string `xorm:"varchar(100)" json:"description"` Description string `xorm:"varchar(100)" json:"description"`
Organization string `xorm:"varchar(100)" json:"organization"` Organization string `xorm:"varchar(100)" json:"organization"`
Cert string `xorm:"varchar(100)" json:"cert"` Cert string `xorm:"varchar(100)" json:"cert"`
EnablePassword bool `json:"enablePassword"` EnablePassword bool `json:"enablePassword"`
EnableSignUp bool `json:"enableSignUp"` EnableSignUp bool `json:"enableSignUp"`
EnableSigninSession bool `json:"enableSigninSession"` EnableSigninSession bool `json:"enableSigninSession"`
EnableAutoSignin bool `json:"enableAutoSignin"` EnableAutoSignin bool `json:"enableAutoSignin"`
EnableCodeSignin bool `json:"enableCodeSignin"` EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"` EnableSamlCompress bool `json:"enableSamlCompress"`
EnableSamlC14n10 bool `json:"enableSamlC14n10"` EnableSamlC14n10 bool `json:"enableSamlC14n10"`
EnableWebAuthn bool `json:"enableWebAuthn"` EnableSamlPostBinding bool `json:"enableSamlPostBinding"`
EnableLinkWithEmail bool `json:"enableLinkWithEmail"` EnableWebAuthn bool `json:"enableWebAuthn"`
OrgChoiceMode string `json:"orgChoiceMode"` EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"` OrgChoiceMode string `json:"orgChoiceMode"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"` SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
CertPublicKey string `xorm:"-" json:"certPublicKey"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
Tags []string `xorm:"mediumtext" json:"tags"` CertPublicKey string `xorm:"-" json:"certPublicKey"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"` Tags []string `xorm:"mediumtext" json:"tags"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
@ -163,15 +163,6 @@ func getProviderMap(owner string) (m map[string]*Provider, err error) {
m = map[string]*Provider{} m = map[string]*Provider{}
for _, provider := range providers { for _, provider := range providers {
// Get QRCode only once
if provider.Type == "WeChat" && provider.DisableSsl && provider.Content == "" {
provider.Content, err = idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2)
if err != nil {
return
}
UpdateProvider(provider.Owner+"/"+provider.Name, provider)
}
m[provider.Name] = GetMaskedProvider(provider, true) m[provider.Name] = GetMaskedProvider(provider, true)
} }

Binary file not shown.

View File

@ -184,6 +184,15 @@ func CheckInvitationCode(application *Application, organization *Organization, a
} }
} }
func CheckInvitationDefaultCode(code string, defaultCode string, lang string) error {
if matched, err := util.IsInvitationCodeMatch(code, defaultCode); err != nil {
return err
} else if !matched {
return fmt.Errorf(i18n.Translate(lang, "check:Default code does not match the code's matching rules"))
}
return nil
}
func checkSigninErrorTimes(user *User, lang string) error { func checkSigninErrorTimes(user *User, lang string) error {
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user) failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
if err != nil { if err != nil {

View File

@ -40,6 +40,7 @@ type Invitation struct {
Phone string `xorm:"varchar(100)" json:"phone"` Phone string `xorm:"varchar(100)" json:"phone"`
SignupGroup string `xorm:"varchar(100)" json:"signupGroup"` SignupGroup string `xorm:"varchar(100)" json:"signupGroup"`
DefaultCode string `xorm:"varchar(100)" json:"defaultCode"`
State string `xorm:"varchar(100)" json:"state"` State string `xorm:"varchar(100)" json:"state"`
} }
@ -93,7 +94,45 @@ func GetInvitation(id string) (*Invitation, error) {
return getInvitation(owner, name) return getInvitation(owner, name)
} }
func UpdateInvitation(id string, invitation *Invitation) (bool, error) { func GetInvitationByCode(code string, organizationName string, lang string) (*Invitation, string) {
invitations, err := GetInvitations(organizationName)
if err != nil {
return nil, err.Error()
}
errMsg := ""
for _, invitation := range invitations {
if isValid, msg := invitation.SimpleCheckInvitationCode(code, lang); isValid {
return invitation, msg
} else if msg != "" && errMsg == "" {
errMsg = msg
}
}
if errMsg != "" {
return nil, errMsg
} else {
return nil, i18n.Translate(lang, "check:Invitation code is invalid")
}
}
func GetMaskedInvitation(invitation *Invitation) *Invitation {
if invitation == nil {
return nil
}
invitation.CreatedTime = ""
invitation.UpdatedTime = ""
invitation.Code = "***"
invitation.DefaultCode = "***"
invitation.IsRegexp = false
invitation.Quota = -1
invitation.UsedCount = -1
invitation.SignupGroup = ""
return invitation
}
func UpdateInvitation(id string, invitation *Invitation, lang string) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if p, err := getInvitation(owner, name); err != nil { if p, err := getInvitation(owner, name); err != nil {
return false, err return false, err
@ -107,6 +146,11 @@ func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
invitation.IsRegexp = isRegexp invitation.IsRegexp = isRegexp
} }
err := CheckInvitationDefaultCode(invitation.Code, invitation.DefaultCode, lang)
if err != nil {
return false, err
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(invitation) affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(invitation)
if err != nil { if err != nil {
return false, err return false, err
@ -115,13 +159,18 @@ func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func AddInvitation(invitation *Invitation) (bool, error) { func AddInvitation(invitation *Invitation, lang string) (bool, error) {
if isRegexp, err := util.IsRegexp(invitation.Code); err != nil { if isRegexp, err := util.IsRegexp(invitation.Code); err != nil {
return false, err return false, err
} else { } else {
invitation.IsRegexp = isRegexp invitation.IsRegexp = isRegexp
} }
err := CheckInvitationDefaultCode(invitation.Code, invitation.DefaultCode, lang)
if err != nil {
return false, err
}
affected, err := ormer.Engine.Insert(invitation) affected, err := ormer.Engine.Insert(invitation)
if err != nil { if err != nil {
return false, err return false, err
@ -147,7 +196,7 @@ func VerifyInvitation(id string) (payment *Payment, attachInfo map[string]interf
return nil, nil, fmt.Errorf("the invitation: %s does not exist", id) return nil, nil, fmt.Errorf("the invitation: %s does not exist", id)
} }
func (invitation *Invitation) IsInvitationCodeValid(application *Application, invitationCode string, username string, email string, phone string, lang string) (bool, string) { func (invitation *Invitation) SimpleCheckInvitationCode(invitationCode string, lang string) (bool, string) {
if matched, err := util.IsInvitationCodeMatch(invitation.Code, invitationCode); err != nil { if matched, err := util.IsInvitationCodeMatch(invitation.Code, invitationCode); err != nil {
return false, err.Error() return false, err.Error()
} else if !matched { } else if !matched {
@ -160,15 +209,6 @@ func (invitation *Invitation) IsInvitationCodeValid(application *Application, in
if invitation.UsedCount >= invitation.Quota { if invitation.UsedCount >= invitation.Quota {
return false, i18n.Translate(lang, "check:Invitation code exhausted") return false, i18n.Translate(lang, "check:Invitation code exhausted")
} }
if application.IsSignupItemRequired("Username") && invitation.Username != "" && invitation.Username != username {
return false, i18n.Translate(lang, "check:Please register using the username corresponding to the invitation code")
}
if application.IsSignupItemRequired("Email") && invitation.Email != "" && invitation.Email != email {
return false, i18n.Translate(lang, "check:Please register using the email corresponding to the invitation code")
}
if application.IsSignupItemRequired("Phone") && invitation.Phone != "" && invitation.Phone != phone {
return false, i18n.Translate(lang, "check:Please register using the phone corresponding to the invitation code")
}
// Determine whether the invitation code is in the form of a regular expression other than pure numbers and letters // Determine whether the invitation code is in the form of a regular expression other than pure numbers and letters
if invitation.IsRegexp { if invitation.IsRegexp {
@ -179,3 +219,19 @@ func (invitation *Invitation) IsInvitationCodeValid(application *Application, in
} }
return true, "" return true, ""
} }
func (invitation *Invitation) IsInvitationCodeValid(application *Application, invitationCode string, username string, email string, phone string, lang string) (bool, string) {
if isValid, msg := invitation.SimpleCheckInvitationCode(invitationCode, lang); !isValid {
return false, msg
}
if application.IsSignupItemRequired("Username") && invitation.Username != "" && invitation.Username != username {
return false, i18n.Translate(lang, "check:Please register using the username corresponding to the invitation code")
}
if application.IsSignupItemRequired("Email") && invitation.Email != "" && invitation.Email != email {
return false, i18n.Translate(lang, "check:Please register using the email corresponding to the invitation code")
}
if application.IsSignupItemRequired("Phone") && invitation.Phone != "" && invitation.Phone != phone {
return false, i18n.Translate(lang, "check:Please register using the phone corresponding to the invitation code")
}
return true, ""
}

View File

@ -18,12 +18,8 @@ import (
"fmt" "fmt"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/beego/beego/context"
) )
const MfaRecoveryCodesSession = "mfa_recovery_codes"
type MfaProps struct { type MfaProps struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"` IsPreferred bool `json:"isPreferred"`
@ -35,9 +31,9 @@ type MfaProps struct {
} }
type MfaInterface interface { type MfaInterface interface {
Initiate(ctx *context.Context, userId string) (*MfaProps, error) Initiate(userId string) (*MfaProps, error)
SetupVerify(ctx *context.Context, passcode string) error SetupVerify(passcode string) error
Enable(ctx *context.Context, user *User) error Enable(user *User) error
Verify(passcode string) error Verify(passcode string) error
} }

View File

@ -16,88 +16,55 @@ package object
import ( import (
"errors" "errors"
"fmt"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/google/uuid"
)
const (
MfaCountryCodeSession = "mfa_country_code"
MfaDestSession = "mfa_dest"
) )
type SmsMfa struct { type SmsMfa struct {
Config *MfaProps *MfaProps
} }
func (mfa *SmsMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) { func (mfa *SmsMfa) Initiate(userId string) (*MfaProps, error) {
recoveryCode := uuid.NewString()
err := ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
if err != nil {
return nil, err
}
mfaProps := MfaProps{ mfaProps := MfaProps{
MfaType: mfa.Config.MfaType, MfaType: mfa.MfaType,
RecoveryCodes: []string{recoveryCode},
} }
return &mfaProps, nil return &mfaProps, nil
} }
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error { func (mfa *SmsMfa) SetupVerify(passCode string) error {
destSession := ctx.Input.CruSession.Get(MfaDestSession) if !util.IsEmailValid(mfa.Secret) {
if destSession == nil { mfa.Secret, _ = util.GetE164Number(mfa.Secret, mfa.CountryCode)
return errors.New("dest session is missing")
}
dest := destSession.(string)
if !util.IsEmailValid(dest) {
countryCodeSession := ctx.Input.CruSession.Get(MfaCountryCodeSession)
if countryCodeSession == nil {
return errors.New("country code is missing")
}
countryCode := countryCodeSession.(string)
dest, _ = util.GetE164Number(dest, countryCode)
} }
if result := CheckVerificationCode(dest, passCode, "en"); result.Code != VerificationSuccess { if result := CheckVerificationCode(mfa.Secret, passCode, "en"); result.Code != VerificationSuccess {
return errors.New(result.Msg) return errors.New(result.Msg)
} }
return nil return nil
} }
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error { func (mfa *SmsMfa) Enable(user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is missing")
}
columns := []string{"recovery_codes", "preferred_mfa_type"} columns := []string{"recovery_codes", "preferred_mfa_type"}
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...) user.RecoveryCodes = append(user.RecoveryCodes, mfa.RecoveryCodes...)
if user.PreferredMfaType == "" { if user.PreferredMfaType == "" {
user.PreferredMfaType = mfa.Config.MfaType user.PreferredMfaType = mfa.MfaType
} }
if mfa.Config.MfaType == SmsType { if mfa.MfaType == SmsType {
user.MfaPhoneEnabled = true user.MfaPhoneEnabled = true
columns = append(columns, "mfa_phone_enabled") columns = append(columns, "mfa_phone_enabled")
if user.Phone == "" { if user.Phone == "" {
user.Phone = ctx.Input.CruSession.Get(MfaDestSession).(string) user.Phone = mfa.Secret
user.CountryCode = ctx.Input.CruSession.Get(MfaCountryCodeSession).(string) user.CountryCode = mfa.CountryCode
columns = append(columns, "phone", "country_code") columns = append(columns, "phone", "country_code")
} }
} else if mfa.Config.MfaType == EmailType { } else if mfa.MfaType == EmailType {
user.MfaEmailEnabled = true user.MfaEmailEnabled = true
columns = append(columns, "mfa_email_enabled") columns = append(columns, "mfa_email_enabled")
if user.Email == "" { if user.Email == "" {
user.Email = ctx.Input.CruSession.Get(MfaDestSession).(string) user.Email = mfa.Secret
columns = append(columns, "email") columns = append(columns, "email")
} }
} }
@ -107,18 +74,14 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
return err return err
} }
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
ctx.Input.CruSession.Delete(MfaDestSession)
ctx.Input.CruSession.Delete(MfaCountryCodeSession)
return nil return nil
} }
func (mfa *SmsMfa) Verify(passCode string) error { func (mfa *SmsMfa) Verify(passCode string) error {
if !util.IsEmailValid(mfa.Config.Secret) { if !util.IsEmailValid(mfa.Secret) {
mfa.Config.Secret, _ = util.GetE164Number(mfa.Config.Secret, mfa.Config.CountryCode) mfa.Secret, _ = util.GetE164Number(mfa.Secret, mfa.CountryCode)
} }
if result := CheckVerificationCode(mfa.Config.Secret, passCode, "en"); result.Code != VerificationSuccess { if result := CheckVerificationCode(mfa.Secret, passCode, "en"); result.Code != VerificationSuccess {
return errors.New(result.Msg) return errors.New(result.Msg)
} }
return nil return nil
@ -131,7 +94,7 @@ func NewSmsMfaUtil(config *MfaProps) *SmsMfa {
} }
} }
return &SmsMfa{ return &SmsMfa{
Config: config, config,
} }
} }
@ -142,6 +105,6 @@ func NewEmailMfaUtil(config *MfaProps) *SmsMfa {
} }
} }
return &SmsMfa{ return &SmsMfa{
Config: config, config,
} }
} }

View File

@ -16,28 +16,24 @@ package object
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"github.com/beego/beego/context"
"github.com/google/uuid"
"github.com/pquerna/otp" "github.com/pquerna/otp"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
) )
const ( const (
MfaTotpSecretSession = "mfa_totp_secret"
MfaTotpPeriodInSeconds = 30 MfaTotpPeriodInSeconds = 30
) )
type TotpMfa struct { type TotpMfa struct {
Config *MfaProps *MfaProps
period uint period uint
secretSize uint secretSize uint
digits otp.Digits digits otp.Digits
} }
func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) { func (mfa *TotpMfa) Initiate(userId string) (*MfaProps, error) {
//issuer := beego.AppConfig.String("appname") //issuer := beego.AppConfig.String("appname")
//if issuer == "" { //if issuer == "" {
// issuer = "casdoor" // issuer = "casdoor"
@ -55,33 +51,16 @@ func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, er
return nil, err return nil, err
} }
err = ctx.Input.CruSession.Set(MfaTotpSecretSession, key.Secret())
if err != nil {
return nil, err
}
recoveryCode := uuid.NewString()
err = ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
if err != nil {
return nil, err
}
mfaProps := MfaProps{ mfaProps := MfaProps{
MfaType: mfa.Config.MfaType, MfaType: mfa.MfaType,
RecoveryCodes: []string{recoveryCode}, Secret: key.Secret(),
Secret: key.Secret(), URL: key.URL(),
URL: key.URL(),
} }
return &mfaProps, nil return &mfaProps, nil
} }
func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error { func (mfa *TotpMfa) SetupVerify(passcode string) error {
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession) result, err := totp.ValidateCustom(passcode, mfa.Secret, time.Now().UTC(), totp.ValidateOpts{
if secret == nil {
return errors.New("totp secret is missing")
}
result, err := totp.ValidateCustom(passcode, secret.(string), time.Now().UTC(), totp.ValidateOpts{
Period: MfaTotpPeriodInSeconds, Period: MfaTotpPeriodInSeconds,
Skew: 1, Skew: 1,
Digits: otp.DigitsSix, Digits: otp.DigitsSix,
@ -98,22 +77,13 @@ func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
} }
} }
func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error { func (mfa *TotpMfa) Enable(user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is missing")
}
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string)
if secret == "" {
return fmt.Errorf("totp secret is missing")
}
columns := []string{"recovery_codes", "preferred_mfa_type", "totp_secret"} columns := []string{"recovery_codes", "preferred_mfa_type", "totp_secret"}
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...) user.RecoveryCodes = append(user.RecoveryCodes, mfa.RecoveryCodes...)
user.TotpSecret = secret user.TotpSecret = mfa.Secret
if user.PreferredMfaType == "" { if user.PreferredMfaType == "" {
user.PreferredMfaType = mfa.Config.MfaType user.PreferredMfaType = mfa.MfaType
} }
_, err := updateUser(user.GetId(), user, columns) _, err := updateUser(user.GetId(), user, columns)
@ -121,14 +91,11 @@ func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
return err return err
} }
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
ctx.Input.CruSession.Delete(MfaTotpSecretSession)
return nil return nil
} }
func (mfa *TotpMfa) Verify(passcode string) error { func (mfa *TotpMfa) Verify(passcode string) error {
result, err := totp.ValidateCustom(passcode, mfa.Config.Secret, time.Now().UTC(), totp.ValidateOpts{ result, err := totp.ValidateCustom(passcode, mfa.Secret, time.Now().UTC(), totp.ValidateOpts{
Period: MfaTotpPeriodInSeconds, Period: MfaTotpPeriodInSeconds,
Skew: 1, Skew: 1,
Digits: otp.DigitsSix, Digits: otp.DigitsSix,
@ -153,7 +120,7 @@ func NewTotpMfaUtil(config *MfaProps) *TotpMfa {
} }
return &TotpMfa{ return &TotpMfa{
Config: config, MfaProps: config,
period: MfaTotpPeriodInSeconds, period: MfaTotpPeriodInSeconds,
secretSize: 20, secretSize: 20,
digits: otp.DigitsSix, digits: otp.DigitsSix,

View File

@ -116,7 +116,7 @@ func getFilteredWebhooks(webhooks []*Webhook, action string) []*Webhook {
} }
func SendWebhooks(record *casvisorsdk.Record) error { func SendWebhooks(record *casvisorsdk.Record) error {
webhooks, err := getWebhooksByOrganization(record.Organization) webhooks, err := getWebhooksByOrganization("")
if err != nil { if err != nil {
return err return err
} }

View File

@ -198,7 +198,7 @@ type Attribute struct {
Values []string `xml:"AttributeValue"` Values []string `xml:"AttributeValue"`
} }
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { func GetSamlMeta(application *Application, host string, enablePostBinding bool) (*IdpEntityDescriptor, error) {
cert, err := getCertByApplication(application) cert, err := getCertByApplication(application)
if err != nil { if err != nil {
return nil, err return nil, err
@ -217,6 +217,13 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
idpLocation := ""
if enablePostBinding {
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
} else {
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
}
d := IdpEntityDescriptor{ d := IdpEntityDescriptor{
XMLName: xml.Name{ XMLName: xml.Name{
Local: "md:EntityDescriptor", Local: "md:EntityDescriptor",
@ -248,7 +255,7 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
}, },
SingleSignOnService: SingleSignOnService{ SingleSignOnService: SingleSignOnService{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Location: fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name), Location: idpLocation,
}, },
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol", ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
}, },
@ -442,3 +449,8 @@ func NewSamlResponse11(user *User, requestID string, host string) *etree.Element
return samlResponse return samlResponse
} }
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string) string {
originF, _ := getOriginFromHost(host)
return fmt.Sprintf("%s/login/saml/authorize/%s/%s?relayState=%s&samlRequest=%s", originF, owner, application, relayState, samlRequest)
}

View File

@ -27,7 +27,7 @@ func getSmsClient(provider *Provider) (sender.SmsClient, error) {
if provider.Type == sender.HuaweiCloud || provider.Type == sender.AzureACS { if provider.Type == sender.HuaweiCloud || provider.Type == sender.AzureACS {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId) client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
} else if provider.Type == "Custom HTTP SMS" { } else if provider.Type == "Custom HTTP SMS" {
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title) client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title, provider.TemplateCode)
} else { } else {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId) client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
} }

View File

@ -27,20 +27,26 @@ type HttpSmsClient struct {
endpoint string endpoint string
method string method string
paramName string paramName string
template string
} }
func newHttpSmsClient(endpoint string, method string, paramName string) (*HttpSmsClient, error) { func newHttpSmsClient(endpoint, method, paramName, template string) (*HttpSmsClient, error) {
if template == "" {
template = "%s"
}
client := &HttpSmsClient{ client := &HttpSmsClient{
endpoint: endpoint, endpoint: endpoint,
method: method, method: method,
paramName: paramName, paramName: paramName,
template: template,
} }
return client, nil return client, nil
} }
func (c *HttpSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error { func (c *HttpSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
phoneNumber := targetPhoneNumber[0] phoneNumber := targetPhoneNumber[0]
content := param["code"] code := param["code"]
content := fmt.Sprintf(c.template, code)
var req *http.Request var req *http.Request
var err error var err error

View File

@ -43,7 +43,7 @@ type Syncer struct {
Host string `xorm:"varchar(100)" json:"host"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"` User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"` Password string `xorm:"varchar(150)" json:"password"`
Database string `xorm:"varchar(100)" json:"database"` Database string `xorm:"varchar(100)" json:"database"`
Table string `xorm:"varchar(100)" json:"table"` Table string `xorm:"varchar(100)" json:"table"`
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"` TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`

View File

@ -93,6 +93,8 @@ func (syncer *Syncer) setUserByKeyValue(user *User, key string, value string) {
user.CreatedTime = value user.CreatedTime = value
case "UpdatedTime": case "UpdatedTime":
user.UpdatedTime = value user.UpdatedTime = value
case "DeletedTime":
user.DeletedTime = value
case "Id": case "Id":
user.Id = value user.Id = value
case "Type": case "Type":
@ -266,6 +268,7 @@ func (syncer *Syncer) getMapFromOriginalUser(user *OriginalUser) map[string]stri
m["Name"] = user.Name m["Name"] = user.Name
m["CreatedTime"] = user.CreatedTime m["CreatedTime"] = user.CreatedTime
m["UpdatedTime"] = user.UpdatedTime m["UpdatedTime"] = user.UpdatedTime
m["DeletedTime"] = user.DeletedTime
m["Id"] = user.Id m["Id"] = user.Id
m["Type"] = user.Type m["Type"] = user.Type
m["Password"] = user.Password m["Password"] = user.Password

View File

@ -186,6 +186,26 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
return &token, nil return &token, nil
} }
func GetTokenByTokenValue(tokenValue string) (*Token, error) {
token, err := GetTokenByAccessToken(tokenValue)
if err != nil {
return nil, err
}
if token != nil {
return token, nil
}
token, err = GetTokenByRefreshToken(tokenValue)
if err != nil {
return nil, err
}
if token != nil {
return token, nil
}
return nil, nil
}
func updateUsedByCode(token *Token) bool { func updateUsedByCode(token *Token) bool {
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token) affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
if err != nil { if err != nil {
@ -283,20 +303,6 @@ func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, e
return affected != 0, application, token, nil return affected != 0, application, token, nil
} }
func GetTokenByTokenAndApplication(token string, application string) (*Token, error) {
tokenResult := Token{}
existed, err := ormer.Engine.Where("(refresh_token = ? or access_token = ? ) and application = ?", token, token, application).Get(&tokenResult)
if err != nil {
return nil, err
}
if !existed {
return nil, nil
}
return &tokenResult, nil
}
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string, lang string) (string, *Application, error) { func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string, lang string) (string, *Application, error) {
if responseType != "code" && responseType != "token" && responseType != "id_token" { if responseType != "code" && responseType != "token" && responseType != "id_token" {
return fmt.Sprintf(i18n.Translate(lang, "token:Grant_type: %s is not supported in this application"), responseType), nil, nil return fmt.Sprintf(i18n.Translate(lang, "token:Grant_type: %s is not supported in this application"), responseType), nil, nil

View File

@ -40,7 +40,7 @@ type UserShort struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Avatar string `xorm:"varchar(500)" json:"avatar"` Avatar string `xorm:"varchar(500)" json:"avatar"`
Email string `xorm:"varchar(100) index" json:"email"` Email string `xorm:"varchar(100) index" json:"email"`
Phone string `xorm:"varchar(20) index" json:"phone"` Phone string `xorm:"varchar(100) index" json:"phone"`
} }
type UserWithoutThirdIdp struct { type UserWithoutThirdIdp struct {
@ -48,10 +48,11 @@ type UserWithoutThirdIdp struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"` CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"` UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
Id string `xorm:"varchar(100) index" json:"id"` Id string `xorm:"varchar(100) index" json:"id"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"` Password string `xorm:"varchar(150)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"` PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"` PasswordType string `xorm:"varchar(100)" json:"passwordType"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
@ -62,7 +63,7 @@ type UserWithoutThirdIdp struct {
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"` PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"` Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"` EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(20) index" json:"phone"` Phone string `xorm:"varchar(100) index" json:"phone"`
CountryCode string `xorm:"varchar(6)" json:"countryCode"` CountryCode string `xorm:"varchar(6)" json:"countryCode"`
Region string `xorm:"varchar(100)" json:"region"` Region string `xorm:"varchar(100)" json:"region"`
Location string `xorm:"varchar(100)" json:"location"` Location string `xorm:"varchar(100)" json:"location"`
@ -167,6 +168,7 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
Name: user.Name, Name: user.Name,
CreatedTime: user.CreatedTime, CreatedTime: user.CreatedTime,
UpdatedTime: user.UpdatedTime, UpdatedTime: user.UpdatedTime,
DeletedTime: user.DeletedTime,
Id: user.Id, Id: user.Id,
Type: user.Type, Type: user.Type,

View File

@ -49,11 +49,12 @@ type User struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"` CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"` UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
Id string `xorm:"varchar(100) index" json:"id"` Id string `xorm:"varchar(100) index" json:"id"`
ExternalId string `xorm:"varchar(100) index" json:"externalId"` ExternalId string `xorm:"varchar(100) index" json:"externalId"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"` Password string `xorm:"varchar(150)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"` PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"` PasswordType string `xorm:"varchar(100)" json:"passwordType"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
@ -64,7 +65,7 @@ type User struct {
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"` PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"` Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"` EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(20) index" json:"phone"` Phone string `xorm:"varchar(100) index" json:"phone"`
CountryCode string `xorm:"varchar(6)" json:"countryCode"` CountryCode string `xorm:"varchar(6)" json:"countryCode"`
Region string `xorm:"varchar(100)" json:"region"` Region string `xorm:"varchar(100)" json:"region"`
Location string `xorm:"varchar(100)" json:"location"` Location string `xorm:"varchar(100)" json:"location"`
@ -639,7 +640,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
if len(columns) == 0 { if len(columns) == 0 {
columns = []string{ columns = []string{
"owner", "display_name", "avatar", "first_name", "last_name", "owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
@ -658,6 +659,10 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
columns = append(columns, "updated_time") columns = append(columns, "updated_time")
user.UpdatedTime = util.GetCurrentTime() user.UpdatedTime = util.GetCurrentTime()
if len(user.DeletedTime) > 0 {
columns = append(columns, "deleted_time")
}
if util.ContainsString(columns, "groups") { if util.ContainsString(columns, "groups") {
_, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups) _, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
if err != nil { if err != nil {

View File

@ -41,11 +41,7 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, resp.Status) fmt.Printf("downloadImage() error for url [%s]: %s\n", url, resp.Status)
if resp.StatusCode == 404 { return nil, "", nil
return nil, "", nil
} else {
return nil, "", fmt.Errorf("failed to download gravatar image: %s", resp.Status)
}
} }
// Get the content type and determine the file extension // Get the content type and determine the file extension

View File

@ -134,6 +134,7 @@ func UploadUsers(owner string, path string) (bool, error) {
LastSigninIp: parseLineItem(&line, 38), LastSigninIp: parseLineItem(&line, 38),
Ldap: "", Ldap: "",
Properties: map[string]string{}, Properties: map[string]string{},
DeletedTime: parseLineItem(&line, 39),
} }
if _, ok := oldUserMap[user.GetId()]; !ok { if _, ok := oldUserMap[user.GetId()]; !ok {

View File

@ -164,6 +164,10 @@ func getUrlPath(urlPath string) string {
return "/api/webauthn" return "/api/webauthn"
} }
if strings.HasPrefix(urlPath, "/api/saml/redirect") {
return "/api/saml/redirect"
}
return urlPath return urlPath
} }

View File

@ -60,7 +60,9 @@ func initAPI() {
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin") beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin") beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta") beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent") beego.Router("/api/saml/redirect/:owner/:application", &controllers.ApiController{}, "*:HandleSamlRedirect")
beego.Router("/api/webhook", &controllers.ApiController{}, "*:HandleOfficialAccountEvent")
beego.Router("/api/get-qrcode", &controllers.ApiController{}, "GET:GetQRCode")
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType") beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus") beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback") beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
@ -93,6 +95,7 @@ func initAPI() {
beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations") beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation") beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
beego.Router("/api/get-invitation-info", &controllers.ApiController{}, "GET:GetInvitationCodeInfo")
beego.Router("/api/update-invitation", &controllers.ApiController{}, "POST:UpdateInvitation") beego.Router("/api/update-invitation", &controllers.ApiController{}, "POST:UpdateInvitation")
beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation") beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation")
beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation") beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation")

View File

@ -5592,6 +5592,9 @@
"enableSamlCompress": { "enableSamlCompress": {
"type": "boolean" "type": "boolean"
}, },
"enableSamlPostBinding": {
"type": "boolean"
},
"enableSignUp": { "enableSignUp": {
"type": "boolean" "type": "boolean"
}, },
@ -7446,6 +7449,9 @@
"displayName": { "displayName": {
"type": "string" "type": "string"
}, },
"deletedTime": {
"type": "string"
},
"douyin": { "douyin": {
"type": "string" "type": "string"
}, },

View File

@ -4900,6 +4900,8 @@ definitions:
type: string type: string
deezer: deezer:
type: string type: string
deletedTime:
type: string
digitalocean: digitalocean:
type: string type: string
dingtalk: dingtalk:

View File

@ -14,7 +14,10 @@
package util package util
import "encoding/json" import (
"encoding/json"
"reflect"
)
func StructToJson(v interface{}) string { func StructToJson(v interface{}) string {
data, err := json.Marshal(v) data, err := json.Marshal(v)
@ -37,3 +40,30 @@ func StructToJsonFormatted(v interface{}) string {
func JsonToStruct(data string, v interface{}) error { func JsonToStruct(data string, v interface{}) error {
return json.Unmarshal([]byte(data), v) return json.Unmarshal([]byte(data), v)
} }
func TryJsonToAnonymousStruct(j string) (interface{}, error) {
var data map[string]interface{}
if err := json.Unmarshal([]byte(j), &data); err != nil {
return nil, err
}
// Create a slice of StructFields
fields := make([]reflect.StructField, 0, len(data))
for k, v := range data {
fields = append(fields, reflect.StructField{
Name: k,
Type: reflect.TypeOf(v),
})
}
// Create the struct type
t := reflect.StructOf(fields)
// Unmarshal again, this time to the new struct type
val := reflect.New(t)
i := val.Interface()
if err := json.Unmarshal([]byte(j), &i); err != nil {
return nil, err
}
return i, nil
}

View File

@ -16,7 +16,6 @@ package util
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -40,7 +39,7 @@ func GetPath(path string) string {
func ListFiles(path string) []string { func ListFiles(path string) []string {
res := []string{} res := []string{}
files, err := ioutil.ReadDir(path) files, err := os.ReadDir(path)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -14,10 +14,10 @@
package util package util
import "io/ioutil" import "os"
func GetUploadXlsxPath(fileId string) string { func GetUploadXlsxPath(fileId string) string {
file, err := ioutil.TempFile("", fileId) file, err := os.CreateTemp("", fileId)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -324,9 +324,16 @@ func GetUsernameFromEmail(email string) string {
} }
func StringToInterfaceArray(array []string) []interface{} { func StringToInterfaceArray(array []string) []interface{} {
var interfaceArray []interface{} var (
for _, v := range array { interfaceArray []interface{}
interfaceArray = append(interfaceArray, v) elem interface{}
)
for _, elem = range array {
jStruct, err := TryJsonToAnonymousStruct(elem.(string))
if err == nil {
elem = jStruct
}
interfaceArray = append(interfaceArray, elem)
} }
return interfaceArray return interfaceArray
} }

View File

@ -119,6 +119,9 @@ func GetVersionInfo() (*VersionInfo, error) {
} }
cIter, err := r.Log(&git.LogOptions{From: ref.Hash()}) cIter, err := r.Log(&git.LogOptions{From: ref.Hash()})
if err != nil {
return res, err
}
commitOffset := 0 commitOffset := 0
version := "" version := ""

View File

@ -70,6 +70,9 @@ func TestGetVersion(t *testing.T) {
testHash := plumbing.NewHash("f8bc87eb4e5ba3256424cf14aafe0549f812f1cf") testHash := plumbing.NewHash("f8bc87eb4e5ba3256424cf14aafe0549f812f1cf")
cIter, err := r.Log(&git.LogOptions{From: testHash}) cIter, err := r.Log(&git.LogOptions{From: testHash})
if err != nil {
t.Log(err)
}
aheadCnt := 0 aheadCnt := 0
releaseVersion := "" releaseVersion := ""

View File

@ -735,7 +735,9 @@ class App extends Component {
account={this.state.account} account={this.state.account}
theme={this.state.themeData} theme={this.state.themeData}
onLoginSuccess={(redirectUrl) => { onLoginSuccess={(redirectUrl) => {
localStorage.setItem("mfaRedirectUrl", redirectUrl); if (redirectUrl) {
localStorage.setItem("mfaRedirectUrl", redirectUrl);
}
this.getAccount(); this.getAccount();
}} }}
onUpdateAccount={(account) => this.onUpdateAccount(account)} onUpdateAccount={(account) => this.onUpdateAccount(account)}

View File

@ -116,7 +116,6 @@ class ApplicationEditPage extends React.Component {
this.getApplication(); this.getApplication();
this.getOrganizations(); this.getOrganizations();
this.getProviders(); this.getProviders();
this.getSamlMetadata();
} }
getApplication() { getApplication() {
@ -146,6 +145,8 @@ class ApplicationEditPage extends React.Component {
}); });
this.getCerts(application.organization); this.getCerts(application.organization);
this.getSamlMetadata(application.enableSamlPostBinding);
}); });
} }
@ -186,8 +187,8 @@ class ApplicationEditPage extends React.Component {
}); });
} }
getSamlMetadata() { getSamlMetadata(checked) {
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName) ApplicationBackend.getSamlMetadata("admin", this.state.applicationName, checked)
.then((data) => { .then((data) => {
this.setState({ this.setState({
samlMetadata: data, samlMetadata: data,
@ -663,6 +664,17 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable SAML POST binding"), i18next.t("application:Enable SAML POST binding - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.application.enableSamlPostBinding} onChange={checked => {
this.updateApplicationField("enableSamlPostBinding", checked);
this.getSamlMetadata(checked);
}} />
</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:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} : {Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} :
@ -688,7 +700,7 @@ class ApplicationEditPage extends React.Component {
/> />
<br /> <br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => { <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}`); copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully")); Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}} }}
> >

View File

@ -19,6 +19,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import copy from "copy-to-clipboard";
const {Option} = Select; const {Option} = Select;
@ -99,6 +100,18 @@ class InvitationEditPage extends React.Component {
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
<Button style={{marginLeft: "20px"}} onClick={() => {
let defaultApplication;
if (this.state.invitation.owner === "built-in") {
defaultApplication = "app-built-in";
} else {
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
}
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")}
</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
@ -140,10 +153,24 @@ class InvitationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.invitation.code} onChange={e => { <Input value={this.state.invitation.code} onChange={e => {
const regex = /[^a-zA-Z0-9]/;
if (!regex.test(e.target.value)) {
this.updateInvitationField("defaultCode", e.target.value);
}
this.updateInvitationField("code", e.target.value); this.updateInvitationField("code", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("invitation:Default code"), i18next.t("invitation:Default code - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.invitation.defaultCode} onChange={e => {
this.updateInvitationField("defaultCode", 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("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} : {Setting.getLabel(i18next.t("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} :
@ -274,6 +301,18 @@ class InvitationEditPage extends React.Component {
<div style={{marginTop: "20px", marginLeft: "40px"}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
<Button style={{marginLeft: "20px"}} size="large" onClick={() => {
let defaultApplication;
if (this.state.invitation.owner === "built-in") {
defaultApplication = "app-built-in";
} else {
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
}
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")}
</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>

View File

@ -22,19 +22,20 @@ import * as InvitationBackend from "./backend/InvitationBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
import copy from "copy-to-clipboard";
class InvitationListPage extends BaseListPage { class InvitationListPage extends BaseListPage {
newInvitation() { newInvitation() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
const owner = Setting.getRequestOrganization(this.props.account); const owner = Setting.getRequestOrganization(this.props.account);
const code = Math.random().toString(36).slice(-10);
return { return {
owner: owner, owner: owner,
name: `invitation_${randomName}`, name: `invitation_${randomName}`,
createdTime: moment().format(), createdTime: moment().format(),
updatedTime: moment().format(), updatedTime: moment().format(),
displayName: `New Invitation - ${randomName}`, displayName: `New Invitation - ${randomName}`,
code: Math.random().toString(36).slice(-10), code: code,
defaultCode: code,
quota: 1, quota: 1,
usedCount: 0, usedCount: 0,
application: "All", application: "All",
@ -225,17 +226,11 @@ class InvitationListPage extends BaseListPage {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: "", dataIndex: "",
key: "op", key: "op",
width: "350px", width: "180px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => {
copy(`${window.location.origin}/login/${record.owner}?invitation_code=${record.code}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")}
</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/invitations/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/invitations/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}

View File

@ -184,7 +184,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})} <Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
options={["plain", "salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))} options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -756,16 +756,28 @@ class ProviderEditPage extends React.Component {
} }
{ {
this.state.provider.type !== "WeChat" ? null : ( this.state.provider.type !== "WeChat" ? null : (
<Row style={{marginTop: "20px"}} > <React.Fragment>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Row style={{marginTop: "20px"}} >
{Setting.getLabel(i18next.t("provider:Enable QR code"), i18next.t("provider:Enable QR code - Tooltip"))} : <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col> {Setting.getLabel(i18next.t("token:Access token"), i18next.t("token:Access token - Tooltip"))} :
<Col span={1} > </Col>
<Switch checked={this.state.provider.disableSsl} onChange={checked => { <Col span={22} >
this.updateProviderField("disableSsl", checked); <Input value={this.state.provider.content} onChange={e => {
}} /> this.updateProviderField("content", e.target.value);
</Col> }} />
</Row> </Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Enable QR code"), i18next.t("provider:Enable QR code - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
this.updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
</React.Fragment>
) )
} }
{ {
@ -1041,7 +1053,7 @@ class ProviderEditPage extends React.Component {
</Row> </Row>
) )
} }
{["Custom HTTP SMS", "Infobip SMS"].includes(this.state.provider.type) ? {["Infobip SMS"].includes(this.state.provider.type) ?
null : null :
(<Row style={{marginTop: "20px"}} > (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>

View File

@ -1448,7 +1448,7 @@ export function getFriendlyUserName(account) {
} }
export function getUserCommonFields() { export function getUserCommonFields() {
return ["Owner", "Name", "CreatedTime", "UpdatedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar", return ["Owner", "Name", "CreatedTime", "UpdatedTime", "DeletedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region", "Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
"Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp", "Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp",
"PreferredMfaType", "TotpSecret", "SignupApplication"]; "PreferredMfaType", "TotpSecret", "SignupApplication"];

View File

@ -27,6 +27,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
import PasswordModal from "./common/modal/PasswordModal"; import PasswordModal from "./common/modal/PasswordModal";
import ResetModal from "./common/modal/ResetModal"; import ResetModal from "./common/modal/ResetModal";
import AffiliationSelect from "./common/select/AffiliationSelect"; import AffiliationSelect from "./common/select/AffiliationSelect";
import moment from "moment";
import OAuthWidget from "./common/OAuthWidget"; import OAuthWidget from "./common/OAuthWidget";
import SamlWidget from "./common/SamlWidget"; import SamlWidget from "./common/SamlWidget";
import RegionSelect from "./common/select/RegionSelect"; import RegionSelect from "./common/select/RegionSelect";
@ -122,6 +123,17 @@ class UserEditPage extends React.Component {
this.setState({ this.setState({
applications: res.data || [], applications: res.data || [],
}); });
const applications = res.data;
if (this.state.user) {
if (this.state.user.signupApplication === "" || applications.filter(application => application.name === this.state.user.signupApplication).length === 0) {
if (applications.length > 0) {
this.updateUserField("signupApplication", applications[0].name);
} else {
this.updateUserField("signupApplication", "");
}
}
}
}); });
} }
@ -858,6 +870,7 @@ class UserEditPage extends React.Component {
<Col span={(Setting.isMobile()) ? 22 : 2} > <Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isDeleted} onChange={checked => { <Switch checked={this.state.user.isDeleted} onChange={checked => {
this.updateUserField("isDeleted", checked); this.updateUserField("isDeleted", checked);
this.updateUserField("deletedTime", checked ? moment().format() : "");
}} /> }} />
</Col> </Col>
</Row> </Row>
@ -890,11 +903,9 @@ class UserEditPage extends React.Component {
</Space> </Space>
{item.enabled ? ( {item.enabled ? (
<Space> <Space>
{item.enabled ? <Tag icon={<CheckCircleOutlined />} color="success">
<Tag icon={<CheckCircleOutlined />} color="success"> {i18next.t("general:Enabled")}
{i18next.t("general:Enabled")} </Tag>
</Tag> : null
}
{item.isPreferred ? {item.isPreferred ?
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} > <Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
{i18next.t("mfa:preferred")} {i18next.t("mfa:preferred")}
@ -916,18 +927,23 @@ class UserEditPage extends React.Component {
{i18next.t("mfa:Set preferred")} {i18next.t("mfa:Set preferred")}
</Button> </Button>
} }
{this.isSelf() ? <Button type={"default"} onClick={() => {
this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
}}>
{i18next.t("general:Edit")}
</Button> : null}
</Space> </Space>
) : ) :
<Space> <Space>
{item.mfaType !== TotpMfaType && Setting.isAdminUser(this.props.account) && window.location.href.indexOf("/users") !== -1 ? {item.mfaType !== TotpMfaType && Setting.isLocalAdminUser(this.props.account) && !this.isSelf() ?
<EnableMfaModal user={this.state.user} mfaType={item.mfaType} onSuccess={() => { <EnableMfaModal user={this.state.user} mfaType={item.mfaType} onSuccess={() => {
this.getUser(); this.getUser();
}} /> : null} }} /> : null}
<Button type={"default"} onClick={() => { {this.isSelf() ? <Button type={"default"} onClick={() => {
this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`); this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
}}> }}>
{i18next.t("mfa:Setup")} {i18next.t("mfa:Setup")}
</Button> </Button> : null}
</Space>} </Space>}
</List.Item> </List.Item>
)} )}

View File

@ -70,7 +70,7 @@ class UserListPage extends BaseListPage {
password: "123", password: "123",
passwordSalt: "", passwordSalt: "",
displayName: `New User - ${randomName}`, displayName: `New User - ${randomName}`,
avatar: `${Setting.StaticBaseUrl}/img/casbin.svg`, avatar: this.state.organization.defaultAvatar ?? `${Setting.StaticBaseUrl}/img/casbin.svg`,
email: `${randomName}@example.com`, email: `${randomName}@example.com`,
phone: Setting.getRandomNumber(), phone: Setting.getRandomNumber(),
countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "", countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "",

View File

@ -62,6 +62,7 @@ const userTemplate = {
"name": "admin", "name": "admin",
"createdTime": "2020-07-16T21:46:52+08:00", "createdTime": "2020-07-16T21:46:52+08:00",
"updatedTime": "", "updatedTime": "",
"deletedTime": "",
"id": "9eb20f79-3bb5-4e74-99ac-39e3b9a171e8", "id": "9eb20f79-3bb5-4e74-99ac-39e3b9a171e8",
"type": "normal-user", "type": "normal-user",
"password": "***", "password": "***",

View File

@ -135,8 +135,18 @@ export function loginWithSaml(values, param) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function getWechatMessageEvent() { export function getWechatMessageEvent(ticket) {
return fetch(`${Setting.ServerUrl}/api/get-webhook-event`, { return fetch(`${Setting.ServerUrl}/api/get-webhook-event?ticket=${ticket}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getWechatQRCode(providerId) {
return fetch(`${Setting.ServerUrl}/api/get-qrcode?id=${providerId}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

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, Result, Row, Steps} from "antd"; import {Button, Col, Result, Row, Spin, Steps} from "antd";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
@ -42,13 +42,20 @@ class MfaSetupPage extends React.Component {
mfaProps: null, mfaProps: null,
mfaType: params.get("mfaType") ?? SmsMfaType, mfaType: params.get("mfaType") ?? SmsMfaType,
isPromptPage: props.isPromptPage || location.state?.from !== undefined, isPromptPage: props.isPromptPage || location.state?.from !== undefined,
loading: false,
}; };
} }
componentDidMount() { componentDidMount() {
this.getApplication(); this.getApplication();
if (this.state.current === 1) { if (this.state.current === 1) {
this.initMfaProps(); this.setState({
loading: true,
});
setTimeout(() => {
this.initMfaProps();
}, 200);
} }
} }
@ -85,6 +92,7 @@ class MfaSetupPage extends React.Component {
if (res.status === "ok") { if (res.status === "ok") {
this.setState({ this.setState({
mfaProps: res.data, mfaProps: res.data,
loading: false,
}); });
} else { } else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA")); Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
@ -98,7 +106,7 @@ class MfaSetupPage extends React.Component {
renderMfaTypeSwitch() { renderMfaTypeSwitch() {
const renderSmsLink = () => { const renderSmsLink = () => {
if (this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) { if (this.state.mfaType === SmsMfaType) {
return null; return null;
} }
return (<Button type={"link"} onClick={() => { return (<Button type={"link"} onClick={() => {
@ -112,7 +120,7 @@ class MfaSetupPage extends React.Component {
}; };
const renderEmailLink = () => { const renderEmailLink = () => {
if (this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) { if (this.state.mfaType === EmailMfaType) {
return null; return null;
} }
return (<Button type={"link"} onClick={() => { return (<Button type={"link"} onClick={() => {
@ -126,7 +134,7 @@ class MfaSetupPage extends React.Component {
}; };
const renderTotpLink = () => { const renderTotpLink = () => {
if (this.state.mfaType === TotpMfaType || this.props.account.totpSecret !== "") { if (this.state.mfaType === TotpMfaType) {
return null; return null;
} }
return (<Button type={"link"} onClick={() => { return (<Button type={"link"} onClick={() => {
@ -191,7 +199,9 @@ class MfaSetupPage extends React.Component {
onSuccess={() => { onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully")); Setting.showMessage("success", i18next.t("general:Enabled successfully"));
this.props.onfinish(); this.props.onfinish();
if (localStorage.getItem("mfaRedirectUrl") !== null) {
const mfaRedirectUrl = localStorage.getItem("mfaRedirectUrl");
if (mfaRedirectUrl !== undefined && mfaRedirectUrl !== null) {
Setting.goToLink(localStorage.getItem("mfaRedirectUrl")); Setting.goToLink(localStorage.getItem("mfaRedirectUrl"));
localStorage.removeItem("mfaRedirectUrl"); localStorage.removeItem("mfaRedirectUrl");
} else { } else {
@ -229,15 +239,17 @@ class MfaSetupPage extends React.Component {
<p style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</p> <p style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</p>
</Col> </Col>
</Row> </Row>
<Steps current={this.state.current} <Spin spinning={this.state.loading}>
items={[ <Steps current={this.state.current}
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />}, items={[
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />}, {title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
{title: i18next.t("general:Enable"), icon: <CheckOutlined />}, {title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
]} {title: i18next.t("general:Enable"), icon: <CheckOutlined />},
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "50px", ]}
}} > style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "50px",
</Steps> }} >
</Steps>
</Spin>
</Col> </Col>
<Col span={24} style={{display: "flex", justifyContent: "center"}}> <Col span={24} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "10px", textAlign: "center"}}> <div style={{marginTop: "10px", textAlign: "center"}}>

View File

@ -43,6 +43,7 @@ import OktaLoginButton from "./OktaLoginButton";
import DouyinLoginButton from "./DouyinLoginButton"; import DouyinLoginButton from "./DouyinLoginButton";
import LoginButton from "./LoginButton"; import LoginButton from "./LoginButton";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import * as Setting from "../Setting";
import {getEvent} from "./Util"; import {getEvent} from "./Util";
import {Modal} from "antd"; import {Modal} from "antd";
@ -132,20 +133,29 @@ export function goToWeb3Url(application, provider, method) {
export function renderProviderLogo(provider, application, width, margin, size, location) { export function renderProviderLogo(provider, application, width, margin, size, location) {
if (size === "small") { if (size === "small") {
if (provider.category === "OAuth") { if (provider.category === "OAuth") {
if (provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.content !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger")) { if (provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger")) {
const info = async() => { const info = async() => {
const t1 = setInterval(await getEvent, 1000, application, provider); AuthBackend.getWechatQRCode(`${provider.owner}/${provider.name}`).then(
{Modal.info({ async res => {
title: i18next.t("provider:Please use WeChat and scan the QR code to sign in"), if (res.status !== "ok") {
content: ( Setting.showMessage("error", res?.msg);
<div> return;
<img width={256} height={256} src = {"data:image/png;base64," + provider.content} alt="Wechat QR code" style={{margin: margin}} /> }
</div>
), const t1 = setInterval(await getEvent, 1000, application, provider, res.data2);
onOk() { {Modal.info({
window.clearInterval(t1); title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
}, content: (
});} <div style={{marginRight: "34px"}}>
<img src = {"data:image/png;base64," + res.data} alt="Wechat QR code" style={{width: "100%"}} />
</div>
),
onOk() {
window.clearInterval(t1);
},
});}
}
);
}; };
return ( return (
<a key={provider.displayName} > <a key={provider.displayName} >

View File

@ -29,6 +29,7 @@ import LanguageSelect from "../common/select/LanguageSelect";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect"; import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import * as PasswordChecker from "../common/PasswordChecker"; import * as PasswordChecker from "../common/PasswordChecker";
import * as InvitationBackend from "../backend/InvitationBackend";
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
@ -93,6 +94,15 @@ class SignupPage extends React.Component {
if (this.getApplicationObj() === undefined) { if (this.getApplicationObj() === undefined) {
if (this.state.applicationName !== null) { if (this.state.applicationName !== null) {
this.getApplication(this.state.applicationName); this.getApplication(this.state.applicationName);
const sp = new URLSearchParams(window.location.search);
if (sp.has("invitationCode")) {
const invitationCode = sp.get("invitationCode");
this.setState({invitationCode: invitationCode});
if (invitationCode !== "") {
this.getInvitationCodeInfo(invitationCode, "admin/" + this.state.applicationName);
}
}
} else if (oAuthParams !== null) { } else if (oAuthParams !== null) {
this.getApplicationLogin(oAuthParams); this.getApplicationLogin(oAuthParams);
} else { } else {
@ -133,6 +143,17 @@ class SignupPage extends React.Component {
}); });
} }
getInvitationCodeInfo(invitationCode, application) {
InvitationBackend.getInvitationCodeInfo(invitationCode, application)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({invitation: res.data});
});
}
getResultPath(application, signupParams) { getResultPath(application, signupParams) {
if (signupParams?.plan && signupParams?.pricing) { if (signupParams?.plan && signupParams?.pricing) {
// the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page // the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page
@ -235,7 +256,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.username !== ""} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Display name") { } else if (signupItem.name === "Display name") {
@ -363,7 +384,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} onChange={e => this.setState({email: e.target.value})} /> <Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
</Form.Item> </Form.Item>
{ {
signupItem.rule !== "No verification" && signupItem.rule !== "No verification" &&
@ -434,6 +455,7 @@ class SignupPage extends React.Component {
<Input <Input
placeholder={signupItem.placeholder} placeholder={signupItem.placeholder}
style={{width: "65%"}} style={{width: "65%"}}
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
onChange={e => this.setState({phone: e.target.value})} onChange={e => this.setState({phone: e.target.value})}
/> />
</Form.Item> </Form.Item>
@ -524,7 +546,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation !== ""} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Agreement") { } else if (signupItem.name === "Agreement") {
@ -554,6 +576,20 @@ class SignupPage extends React.Component {
</Result> </Result>
); );
} }
if (this.state.invitation !== undefined) {
if (this.state.invitation.username !== "") {
this.form.current?.setFieldValue("username", this.state.invitation.username);
}
if (this.state.invitation.email !== "") {
this.form.current?.setFieldValue("email", this.state.invitation.email);
}
if (this.state.invitation.phone !== "") {
this.form.current?.setFieldValue("phone", this.state.invitation.phone);
}
if (this.state.invitationCode !== "") {
this.form.current?.setFieldValue("invitationCode", this.state.invitationCode);
}
}
return ( return (
<Form <Form
{...formItemLayout} {...formItemLayout}

View File

@ -188,8 +188,8 @@ export function getQueryParamsFromState(state) {
} }
} }
export function getEvent(application, provider) { export function getEvent(application, provider, ticket) {
getWechatMessageEvent() getWechatMessageEvent(ticket)
.then(res => { .then(res => {
if (res.data === "SCAN" || res.data === "subscribe") { if (res.data === "SCAN" || res.data === "subscribe") {
Setting.goToLink(Provider.getAuthUrl(application, provider, "signup")); Setting.goToLink(Provider.getAuthUrl(application, provider, "signup"));

View File

@ -89,8 +89,8 @@ export function deleteApplication(application) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function getSamlMetadata(owner, name) { export function getSamlMetadata(owner, name, enablePostBinding) {
return fetch(`${Setting.ServerUrl}/api/saml/metadata?application=${owner}/${encodeURIComponent(name)}`, { return fetch(`${Setting.ServerUrl}/api/saml/metadata?application=${owner}/${encodeURIComponent(name)}&enablePostBinding=${enablePostBinding}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@ -34,6 +34,16 @@ export function getInvitation(owner, name) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function getInvitationCodeInfo(code, applicationName) {
return fetch(`${Setting.ServerUrl}/api/get-invitation-info?code=${code}&applicationId=${encodeURIComponent(applicationName)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateInvitation(owner, name, invitation) { export function updateInvitation(owner, name, invitation) {
const newInvitation = Setting.deepCopy(invitation); const newInvitation = Setting.deepCopy(invitation);
return fetch(`${Setting.ServerUrl}/api/update-invitation?id=${owner}/${encodeURIComponent(name)}`, { return fetch(`${Setting.ServerUrl}/api/update-invitation?id=${owner}/${encodeURIComponent(name)}`, {

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "Bei der Verwendung von Drittanbietern zur Anmeldung wird, wenn es in der Organisation einen Benutzer mit der gleichen E-Mail gibt, automatisch die Drittanbieter-Anmelde-Methode mit diesem Benutzer verbunden", "Enable Email linking - Tooltip": "Bei der Verwendung von Drittanbietern zur Anmeldung wird, wenn es in der Organisation einen Benutzer mit der gleichen E-Mail gibt, automatisch die Drittanbieter-Anmelde-Methode mit diesem Benutzer verbunden",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Aktivieren Sie SAML-Komprimierung", "Enable SAML compression": "Aktivieren Sie SAML-Komprimierung",
"Enable SAML compression - Tooltip": "Ob SAML-Antwortnachrichten komprimiert werden sollen, wenn Casdoor als SAML-IdP verwendet wird", "Enable SAML compression - Tooltip": "Ob SAML-Antwortnachrichten komprimiert werden sollen, wenn Casdoor als SAML-IdP verwendet wird",
"Enable side panel": "Sidepanel aktivieren", "Enable side panel": "Sidepanel aktivieren",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Metadaten erfolgreich analysiert", "Parse metadata successfully": "Metadaten erfolgreich analysiert",
"Path prefix": "Pfadpräfix", "Path prefix": "Pfadpräfix",
"Path prefix - Tooltip": "Bucket-Pfad-Präfix für Objektspeicher", "Path prefix - Tooltip": "Bucket-Pfad-Präfix für Objektspeicher",
"Please use WeChat and scan the QR code to sign in": "Bitte verwenden Sie WeChat und scanne den QR-Code ein, um dich anzumelden", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Hafen", "Port": "Hafen",
"Port - Tooltip": "Stellen Sie sicher, dass der Port offen ist", "Port - Tooltip": "Stellen Sie sicher, dass der Port offen ist",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Use C14N10 instead of C14N11 in SAML", "Enable SAML C14N10 - Tooltip": "Use C14N10 instead of C14N11 in SAML",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Can be a single string as an invitation code, or a regular expression. All strings matching the regular expression are valid invitation codes", "Code - Tooltip": "Can be a single string as an invitation code, or a regular expression. All strings matching the regular expression are valid invitation codes",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "Cuando se utilizan proveedores externos de inicio de sesión, si hay un usuario en la organización con el mismo correo electrónico, el método de inicio de sesión externo se asociará automáticamente con ese usuario", "Enable Email linking - Tooltip": "Cuando se utilizan proveedores externos de inicio de sesión, si hay un usuario en la organización con el mismo correo electrónico, el método de inicio de sesión externo se asociará automáticamente con ese usuario",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Activar la compresión SAML", "Enable SAML compression": "Activar la compresión SAML",
"Enable SAML compression - Tooltip": "Si comprimir o no los mensajes de respuesta SAML cuando se utiliza Casdoor como proveedor de identidad SAML", "Enable SAML compression - Tooltip": "Si comprimir o no los mensajes de respuesta SAML cuando se utiliza Casdoor como proveedor de identidad SAML",
"Enable side panel": "Habilitar panel lateral", "Enable side panel": "Habilitar panel lateral",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Analizar los metadatos con éxito", "Parse metadata successfully": "Analizar los metadatos con éxito",
"Path prefix": "Prefijo de ruta", "Path prefix": "Prefijo de ruta",
"Path prefix - Tooltip": "Prefijo de ruta de cubo para almacenamiento de objetos", "Path prefix - Tooltip": "Prefijo de ruta de cubo para almacenamiento de objetos",
"Please use WeChat and scan the QR code to sign in": "Por favor, utiliza WeChat y escanea el código QR para iniciar sesión", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Puerto", "Port": "Puerto",
"Port - Tooltip": "Asegúrate de que el puerto esté abierto", "Port - Tooltip": "Asegúrate de que el puerto esté abierto",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "Lorsqu'un fournisseur tiers est utilisé pour se connecter, si un compte existe dans l'organisation avec la même adresse e-mail, la méthode de connexion tierce sera automatiquement associée à ce compte", "Enable Email linking - Tooltip": "Lorsqu'un fournisseur tiers est utilisé pour se connecter, si un compte existe dans l'organisation avec la même adresse e-mail, la méthode de connexion tierce sera automatiquement associée à ce compte",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Activer la compression SAML", "Enable SAML compression": "Activer la compression SAML",
"Enable SAML compression - Tooltip": "Compresser ou non les messages de réponse SAML lorsque Casdoor est utilisé en tant que fournisseur d'identité SAML", "Enable SAML compression - Tooltip": "Compresser ou non les messages de réponse SAML lorsque Casdoor est utilisé en tant que fournisseur d'identité SAML",
"Enable side panel": "Activer le panneau latéral", "Enable side panel": "Activer le panneau latéral",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parcourir les métadonnées avec succès", "Parse metadata successfully": "Parcourir les métadonnées avec succès",
"Path prefix": "Préfixe de chemin", "Path prefix": "Préfixe de chemin",
"Path prefix - Tooltip": "Préfixe de chemin de seau pour le stockage d'objet", "Path prefix - Tooltip": "Préfixe de chemin de seau pour le stockage d'objet",
"Please use WeChat and scan the QR code to sign in": "Veuillez utiliser WeChat et scanner le code QR pour vous connecter", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Assurez-vous que le port est ouvert", "Port - Tooltip": "Assurez-vous que le port est ouvert",
"Private Key": "Clé privée", "Private Key": "Clé privée",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "Ketika menggunakan penyedia layanan pihak ketiga untuk masuk, jika ada pengguna di organisasi dengan email yang sama, metode login pihak ketiga akan secara otomatis terhubung dengan pengguna tersebut", "Enable Email linking - Tooltip": "Ketika menggunakan penyedia layanan pihak ketiga untuk masuk, jika ada pengguna di organisasi dengan email yang sama, metode login pihak ketiga akan secara otomatis terhubung dengan pengguna tersebut",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Aktifkan kompresi SAML", "Enable SAML compression": "Aktifkan kompresi SAML",
"Enable SAML compression - Tooltip": "Apakah pesan respons SAML harus dikompres saat Casdoor digunakan sebagai SAML idp?", "Enable SAML compression - Tooltip": "Apakah pesan respons SAML harus dikompres saat Casdoor digunakan sebagai SAML idp?",
"Enable side panel": "Aktifkan panel samping", "Enable side panel": "Aktifkan panel samping",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Berhasil mem-parse metadata", "Parse metadata successfully": "Berhasil mem-parse metadata",
"Path prefix": "Awalan jalur", "Path prefix": "Awalan jalur",
"Path prefix - Tooltip": "Awalan path ember untuk penyimpanan objek dalam bucket", "Path prefix - Tooltip": "Awalan path ember untuk penyimpanan objek dalam bucket",
"Please use WeChat and scan the QR code to sign in": "Silakan gunakan WeChat dan pindai kode QR untuk masuk", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Pelabuhan", "Port": "Pelabuhan",
"Port - Tooltip": "Pastikan port terbuka", "Port - Tooltip": "Pastikan port terbuka",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "組織内に同じメールアドレスを持つユーザーがいる場合、サードパーティのログイン方法は自動的にそのユーザーに関連付けられます", "Enable Email linking - Tooltip": "組織内に同じメールアドレスを持つユーザーがいる場合、サードパーティのログイン方法は自動的にそのユーザーに関連付けられます",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "SAMLの圧縮を有効にする", "Enable SAML compression": "SAMLの圧縮を有効にする",
"Enable SAML compression - Tooltip": "CasdoorをSAML IdPとして使用する場合、SAMLレスポンスメッセージを圧縮するかどうか。圧縮する: 圧縮するかどうか。圧縮しない: 圧縮しないかどうか", "Enable SAML compression - Tooltip": "CasdoorをSAML IdPとして使用する場合、SAMLレスポンスメッセージを圧縮するかどうか。圧縮する: 圧縮するかどうか。圧縮しない: 圧縮しないかどうか",
"Enable side panel": "サイドパネルを有効にする", "Enable side panel": "サイドパネルを有効にする",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "メタデータを正常に解析しました", "Parse metadata successfully": "メタデータを正常に解析しました",
"Path prefix": "パスプレフィックス", "Path prefix": "パスプレフィックス",
"Path prefix - Tooltip": "オブジェクトストレージのバケットパスプレフィックス", "Path prefix - Tooltip": "オブジェクトストレージのバケットパスプレフィックス",
"Please use WeChat and scan the QR code to sign in": "WeChatを使用し、QRコードをスキャンしてサインインしてください", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "ポート", "Port": "ポート",
"Port - Tooltip": "ポートが開いていることを確認してください", "Port - Tooltip": "ポートが開いていることを確認してください",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "3rd-party 로그인 공급자를 사용할 때, 만약 조직 내에 동일한 이메일을 사용하는 사용자가 있다면, 3rd-party 로그인 방법은 자동으로 해당 사용자와 연동됩니다", "Enable Email linking - Tooltip": "3rd-party 로그인 공급자를 사용할 때, 만약 조직 내에 동일한 이메일을 사용하는 사용자가 있다면, 3rd-party 로그인 방법은 자동으로 해당 사용자와 연동됩니다",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "SAML 압축 사용 가능하게 설정하기", "Enable SAML compression": "SAML 압축 사용 가능하게 설정하기",
"Enable SAML compression - Tooltip": "카스도어가 SAML idp로 사용될 때 SAML 응답 메시지를 압축할 것인지 여부", "Enable SAML compression - Tooltip": "카스도어가 SAML idp로 사용될 때 SAML 응답 메시지를 압축할 것인지 여부",
"Enable side panel": "측면 패널 활성화", "Enable side panel": "측면 패널 활성화",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "메타데이터를 성공적으로 분석했습니다", "Parse metadata successfully": "메타데이터를 성공적으로 분석했습니다",
"Path prefix": "경로 접두어", "Path prefix": "경로 접두어",
"Path prefix - Tooltip": "객체 저장소에 대한 버킷 경로 접두어", "Path prefix - Tooltip": "객체 저장소에 대한 버킷 경로 접두어",
"Please use WeChat and scan the QR code to sign in": "WeChat를 사용하시고 QR 코드를 스캔하여 로그인해주세요", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "포트", "Port": "포트",
"Port - Tooltip": "포트가 열려 있는지 확인하세요", "Port - Tooltip": "포트가 열려 있는지 확인하세요",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user", "Enable Email linking - Tooltip": "When using 3rd-party providers to log in, if there is a user in the organization with the same Email, the 3rd-party login method will be automatically associated with that user",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Parse metadata successfully", "Parse metadata successfully": "Parse metadata successfully",
"Path prefix": "Path prefix", "Path prefix": "Path prefix",
"Path prefix - Tooltip": "Bucket path prefix for object storage", "Path prefix - Tooltip": "Bucket path prefix for object storage",
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Port", "Port": "Port",
"Port - Tooltip": "Make sure the port is open", "Port - Tooltip": "Make sure the port is open",
"Private Key": "Private Key", "Private Key": "Private Key",

View File

@ -34,6 +34,8 @@
"Enable Email linking - Tooltip": "Ao usar provedores de terceiros para fazer login, se houver um usuário na organização com o mesmo e-mail, o método de login de terceiros será automaticamente associado a esse usuário", "Enable Email linking - Tooltip": "Ao usar provedores de terceiros para fazer login, se houver um usuário na organização com o mesmo e-mail, o método de login de terceiros será automaticamente associado a esse usuário",
"Enable SAML C14N10": "Enable SAML C14N10", "Enable SAML C14N10": "Enable SAML C14N10",
"Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip", "Enable SAML C14N10 - Tooltip": "Enable SAML C14N10 - Tooltip",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable SAML compression": "Ativar compressão SAML", "Enable SAML compression": "Ativar compressão SAML",
"Enable SAML compression - Tooltip": "Se deve comprimir as mensagens de resposta SAML quando o Casdoor é usado como provedor de identidade SAML", "Enable SAML compression - Tooltip": "Se deve comprimir as mensagens de resposta SAML quando o Casdoor é usado como provedor de identidade SAML",
"Enable side panel": "Ativar painel lateral", "Enable side panel": "Ativar painel lateral",
@ -388,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",
@ -753,7 +757,7 @@
"Parse metadata successfully": "Metadados analisados com sucesso", "Parse metadata successfully": "Metadados analisados com sucesso",
"Path prefix": "Prefixo do caminho", "Path prefix": "Prefixo do caminho",
"Path prefix - Tooltip": "Prefixo do caminho do bucket para armazenamento de objetos", "Path prefix - Tooltip": "Prefixo do caminho do bucket para armazenamento de objetos",
"Please use WeChat and scan the QR code to sign in": "Por favor, use o WeChat e escaneie o código QR para fazer login", "Please use WeChat to scan the QR code and follow the official account for sign in": "Please use WeChat to scan the QR code and follow the official account for sign in",
"Port": "Porta", "Port": "Porta",
"Port - Tooltip": "Certifique-se de que a porta esteja aberta", "Port - Tooltip": "Certifique-se de que a porta esteja aberta",
"Private Key": "Private Key", "Private Key": "Private Key",

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