Compare commits

...

19 Commits

Author SHA1 Message Date
yehong
beefb0b432 fix: fix event-stream streaming output in prod mode (#2076) 2023-07-14 11:59:26 +08:00
Yang Luo
d8969e6652 Support EnableSigninSession after SAML login 2023-07-14 11:27:18 +08:00
Yang Luo
666ff48837 Use id param in /sync-ldap-users API 2023-07-13 00:14:18 +08:00
Yang Luo
0a0c1b4788 Fix "Groups is immutable" bug when updating a user 2023-07-13 00:03:18 +08:00
Yang Luo
438c999e11 Add password mask to /get-ldaps and /get-ldap APIs 2023-07-12 23:21:47 +08:00
Yang Luo
a193ceb33d Fix bug in TestDeployStaticFiles() 2023-07-12 23:11:02 +08:00
Yang Luo
caec1d1bac Only consider x509 certs in /.well-known/jwks API 2023-07-12 22:39:39 +08:00
Denis Plynskiy
0d48da24dc feat: fix wrong rowKey for tables (#2070) 2023-07-12 21:12:36 +08:00
Yaodong Yu
de9eeaa1ef fix: init groups modify rule with admin (#2054) 2023-07-11 09:49:49 +08:00
Baihhh
ae6e35ee73 feat: fix bug that the password input disappears in login window (#2051)
Signed-off-by: baihhh <2542274498@qq.com>
2023-07-08 23:46:31 +08:00
Yaodong Yu
a58df645bf fix: fix state after mfa is enabled (#2050) 2023-07-08 22:35:31 +08:00
WintBit
68417a2d7a fix: /api/upload-resource panics when parsing file_type (#2046) 2023-07-07 16:18:25 +08:00
WintBit
9511fae9d9 docs: add swagger docs for Resource-API (#2044)
swagger files are all auto generated.
2023-07-07 14:28:10 +08:00
Yaodong Yu
347d3d2b53 feat: fix bugs in MFA (#2033)
* fix: prompt mfa binding

* fix: clean session when leave promptpage

* fix: css

* fix: force enable mfa

* fix: add prompt rule

* fix: refactor directory structure

* fix: prompt notification

* fix: fix some bug and clean code

* fix: rebase

* fix: improve notification

* fix: i18n

* fix: router

* fix: prompt

* fix: remove localStorage
2023-07-07 12:30:07 +08:00
Gucheng Wang
6edfc08b28 Refactor the code 2023-07-07 00:13:05 +08:00
Baihhh
bc1c4d32f0 feat: user can upload ID card info (#2040)
* feat:user can upload ID card(#1999)

Signed-off-by: baihhh <2542274498@qq.com>

* feat: user can upload ID card, add diff languages

Signed-off-by: baihhh <2542274498@qq.com>

---------

Signed-off-by: baihhh <2542274498@qq.com>
2023-07-06 20:36:32 +08:00
YunShu
96250aa70a docs: replace gitter links with discord (#2041) 2023-07-06 18:16:16 +08:00
Yaodong Yu
3d4ca1adb1 feat: support custom user mapping (#2029)
* feat: support custom user mapping

* fix: parse id to string

* Update data.json

* Update data.json

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-07-05 20:35:02 +08:00
Yang Luo
ba97458edd feat: fix StaticFilter issue 2023-07-05 17:54:39 +08:00
65 changed files with 2050 additions and 808 deletions

View File

@@ -37,8 +37,8 @@
<a href="https://crowdin.com/project/casdoor-site"> <a href="https://crowdin.com/project/casdoor-site">
<img alt="Crowdin" src="https://badges.crowdin.net/casdoor-site/localized.svg"> <img alt="Crowdin" src="https://badges.crowdin.net/casdoor-site/localized.svg">
</a> </a>
<a href="https://gitter.im/casbin/casdoor"> <a href="https://discord.gg/5rPsrAzK7S">
<img alt="Gitter" src="https://badges.gitter.im/casbin/casdoor.svg"> <img alt="Discord" src="https://img.shields.io/discord/1022748306096537660?style=flat-square&logo=discord&label=discord&color=5865F2">
</a> </a>
</p> </p>
@@ -71,7 +71,7 @@ https://casdoor.org/docs/category/integrations
## How to contact? ## How to contact?
- Gitter: https://gitter.im/casbin/casdoor - Discord: https://discord.gg/5rPsrAzK7S
- Forum: https://forum.casbin.com - Forum: https://forum.casbin.com
- Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e - Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e

View File

@@ -129,7 +129,7 @@ func QueryAnswerStream(authToken string, question string, writer io.Writer, buil
fmt.Printf("%s", data) fmt.Printf("%s", data)
// Write the streamed data as Server-Sent Events // Write the streamed data as Server-Sent Events
if _, err = fmt.Fprintf(writer, "data: %s\n\n", data); err != nil { if _, err = fmt.Fprintf(writer, "event: message\ndata: %s\n\n", data); err != nil {
return err return err
} }
flusher.Flush() flusher.Flush()

View File

@@ -78,12 +78,6 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
} }
} }
if form.Password != "" && user.IsMfaEnabled() {
c.setMfaSessionData(&object.MfaSessionData{UserId: userId})
resp = &Response{Status: object.NextMfa, Data: user.GetPreferredMfaProps(true)}
return
}
if form.Type == ResponseTypeLogin { if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId) c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId) util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
@@ -129,6 +123,11 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
return return
} }
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]string{"redirectUrl": redirectUrl, "method": method}} resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]string{"redirectUrl": redirectUrl, "method": method}}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else if form.Type == ResponseTypeCas { } else if form.Type == ResponseTypeCas {
// not oauth but CAS SSO protocol // not oauth but CAS SSO protocol
service := c.Input().Get("service") service := c.Input().Get("service")
@@ -141,11 +140,11 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
resp.Data = st resp.Data = st
} }
} }
if application.EnableSigninSession || application.HasPromptPage() { if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in // The prompt page needs the user to be signed in
c.SetSessionUsername(userId) c.SetSessionUsername(userId)
} }
} else { } else {
resp = wrapErrorResponse(fmt.Errorf("unknown response type: %s", form.Type)) resp = wrapErrorResponse(fmt.Errorf("unknown response type: %s", form.Type))
} }
@@ -353,17 +352,26 @@ func (c *ApiController) Login() {
return return
} }
resp = c.HandleLoggedIn(application, user, &authForm)
organization, err := object.GetOrganizationByUser(user) organization, err := object.GetOrganizationByUser(user)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} }
if user != nil && organization.HasRequiredMfa() && !user.IsMfaEnabled() { if object.IsNeedPromptMfa(organization, user) {
resp.Msg = object.RequiredMfa // The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId())
c.ResponseOk(object.RequiredMfa)
return
} }
if user.IsMfaEnabled() {
c.setMfaUserSession(user.GetId())
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
@@ -416,15 +424,8 @@ func (c *ApiController) Login() {
} }
} else if provider.Category == "OAuth" { } else if provider.Category == "OAuth" {
// OAuth // OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
clientId := provider.ClientId idProvider := idp.GetIdProvider(idpInfo, authForm.RedirectUri)
clientSecret := provider.ClientSecret
if provider.Type == "WeChat" && strings.Contains(c.Ctx.Request.UserAgent(), "MicroMessenger") {
clientId = provider.ClientId2
clientSecret = provider.ClientSecret2
}
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, authForm.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl)
if idProvider == nil { if idProvider == nil {
c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type)) c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type))
return return
@@ -656,13 +657,16 @@ func (c *ApiController) Login() {
resp = &Response{Status: "error", Msg: "Failed to link user account", Data: isLinked} resp = &Response{Status: "error", Msg: "Failed to link user account", Data: isLinked}
} }
} }
} else if c.getMfaSessionData() != nil { } else if c.getMfaUserSession() != "" {
mfaSession := c.getMfaSessionData() user, err := object.GetUser(c.getMfaUserSession())
user, err := object.GetUser(mfaSession.UserId)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if user == nil {
c.ResponseError("expired user session")
return
}
if authForm.Passcode != "" { if authForm.Passcode != "" {
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false)) mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
@@ -676,13 +680,15 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
} } else if authForm.RecoveryCode != "" {
if authForm.RecoveryCode != "" {
err = object.MfaRecover(user, authForm.RecoveryCode) err = object.MfaRecover(user, authForm.RecoveryCode)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
} else {
c.ResponseError("missing passcode or recovery code")
return
} }
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application)) application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
@@ -697,6 +703,7 @@ func (c *ApiController) Login() {
} }
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)
c.setMfaUserSession("")
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization

View File

@@ -178,24 +178,16 @@ func (c *ApiController) SetSessionData(s *SessionData) {
c.SetSession("SessionData", util.StructToJson(s)) c.SetSession("SessionData", util.StructToJson(s))
} }
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) { func (c *ApiController) setMfaUserSession(userId string) {
if data == nil { c.SetSession(object.MfaSessionUserId, userId)
c.SetSession(object.MfaSessionUserId, nil)
return
}
c.SetSession(object.MfaSessionUserId, data.UserId)
} }
func (c *ApiController) getMfaSessionData() *object.MfaSessionData { func (c *ApiController) getMfaUserSession() string {
userId := c.GetSession(object.MfaSessionUserId) userId := c.Ctx.Input.CruSession.Get(object.MfaSessionUserId)
if userId == nil { if userId == nil {
return nil return ""
} }
return userId.(string)
data := &object.MfaSessionData{
UserId: userId.(string),
}
return data
} }
func (c *ApiController) setExpireForSession() { func (c *ApiController) setExpireForSession() {

View File

@@ -100,7 +100,7 @@ func (c *ApiController) GetLdapUsers() {
func (c *ApiController) GetLdaps() { func (c *ApiController) GetLdaps() {
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
c.ResponseOk(object.GetLdaps(owner)) c.ResponseOk(object.GetMaskedLdaps(object.GetLdaps(owner)))
} }
// GetLdap // GetLdap
@@ -116,7 +116,7 @@ func (c *ApiController) GetLdap() {
} }
_, name := util.GetOwnerAndNameFromId(id) _, name := util.GetOwnerAndNameFromId(id)
c.ResponseOk(object.GetLdap(name)) c.ResponseOk(object.GetMaskedLdap(object.GetLdap(name)))
} }
// AddLdap // AddLdap
@@ -226,8 +226,9 @@ func (c *ApiController) DeleteLdap() {
// @Title SyncLdapUsers // @Title SyncLdapUsers
// @router /sync-ldap-users [post] // @router /sync-ldap-users [post]
func (c *ApiController) SyncLdapUsers() { func (c *ApiController) SyncLdapUsers() {
owner := c.Input().Get("owner") id := c.Input().Get("id")
ldapId := c.Input().Get("ldapId")
owner, ldapId := util.GetOwnerAndNameFromId(id)
var users []object.LdapUser var users []object.LdapUser
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users) err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
if err != nil { if err != nil {

View File

@@ -29,9 +29,19 @@ import (
) )
// GetResources // GetResources
// @router /get-resources [get]
// @Tag Resource API // @Tag Resource API
// @Title GetResources // @Title GetResources
// @Description get resources
// @Param owner query string true "Owner"
// @Param user query string true "User"
// @Param pageSize query integer false "Page Size"
// @Param p query integer false "Page Number"
// @Param field query string false "Field"
// @Param value query string false "Value"
// @Param sortField query string false "Sort Field"
// @Param sortOrder query string false "Sort Order"
// @Success 200 {array} object.Resource The Response object
// @router /get-resources [get]
func (c *ApiController) GetResources() { func (c *ApiController) GetResources() {
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
user := c.Input().Get("user") user := c.Input().Get("user")
@@ -81,6 +91,9 @@ func (c *ApiController) GetResources() {
// GetResource // GetResource
// @Tag Resource API // @Tag Resource API
// @Title GetResource // @Title GetResource
// @Description get resource
// @Param id query string true "The id ( owner/name ) of resource"
// @Success 200 {object} object.Resource The Response object
// @router /get-resource [get] // @router /get-resource [get]
func (c *ApiController) GetResource() { func (c *ApiController) GetResource() {
id := c.Input().Get("id") id := c.Input().Get("id")
@@ -98,6 +111,10 @@ func (c *ApiController) GetResource() {
// UpdateResource // UpdateResource
// @Tag Resource API // @Tag Resource API
// @Title UpdateResource // @Title UpdateResource
// @Description get resource
// @Param id query string true "The id ( owner/name ) of resource"
// @Param resource body object.Resource true "The resource object"
// @Success 200 {object} controllers.Response Success or error
// @router /update-resource [post] // @router /update-resource [post]
func (c *ApiController) UpdateResource() { func (c *ApiController) UpdateResource() {
id := c.Input().Get("id") id := c.Input().Get("id")
@@ -116,6 +133,8 @@ func (c *ApiController) UpdateResource() {
// AddResource // AddResource
// @Tag Resource API // @Tag Resource API
// @Title AddResource // @Title AddResource
// @Param resource body object.Resource true "Resource object"
// @Success 200 {object} controllers.Response Success or error
// @router /add-resource [post] // @router /add-resource [post]
func (c *ApiController) AddResource() { func (c *ApiController) AddResource() {
var resource object.Resource var resource object.Resource
@@ -132,6 +151,8 @@ func (c *ApiController) AddResource() {
// DeleteResource // DeleteResource
// @Tag Resource API // @Tag Resource API
// @Title DeleteResource // @Title DeleteResource
// @Param resource body object.Resource true "Resource object"
// @Success 200 {object} controllers.Response Success or error
// @router /delete-resource [post] // @router /delete-resource [post]
func (c *ApiController) DeleteResource() { func (c *ApiController) DeleteResource() {
var resource object.Resource var resource object.Resource
@@ -160,6 +181,16 @@ func (c *ApiController) DeleteResource() {
// UploadResource // UploadResource
// @Tag Resource API // @Tag Resource API
// @Title UploadResource // @Title UploadResource
// @Param owner query string true "Owner"
// @Param user query string true "User"
// @Param application query string true "Application"
// @Param tag query string false "Tag"
// @Param parent query string false "Parent"
// @Param fullFilePath query string true "Full File Path"
// @Param createdTime query string false "Created Time"
// @Param description query string false "Description"
// @Param file formData file true "Resource file"
// @Success 200 {object} object.Resource FileUrl, objectKey
// @router /upload-resource [post] // @router /upload-resource [post]
func (c *ApiController) UploadResource() { func (c *ApiController) UploadResource() {
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
@@ -198,16 +229,16 @@ func (c *ApiController) UploadResource() {
fileType := "unknown" fileType := "unknown"
contentType := header.Header.Get("Content-Type") contentType := header.Header.Get("Content-Type")
fileType, _ = util.GetOwnerAndNameFromId(contentType) fileType, _ = util.GetOwnerAndNameFromIdNoCheck(contentType + "/")
if fileType != "image" && fileType != "video" { if fileType != "image" && fileType != "video" {
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
mimeType := mime.TypeByExtension(ext) mimeType := mime.TypeByExtension(ext)
fileType, _ = util.GetOwnerAndNameFromId(mimeType) fileType, _ = util.GetOwnerAndNameFromIdNoCheck(mimeType + "/")
} }
fullFilePath = object.GetTruncatedPath(provider, fullFilePath, 175) fullFilePath = object.GetTruncatedPath(provider, fullFilePath, 175)
if tag != "avatar" && tag != "termsOfUse" { if tag != "avatar" && tag != "termsOfUse" && !strings.HasPrefix(tag, "idCard") {
ext := filepath.Ext(filepath.Base(fullFilePath)) ext := filepath.Ext(filepath.Base(fullFilePath))
index := len(fullFilePath) - len(ext) index := len(fullFilePath) - len(ext)
for i := 1; ; i++ { for i := 1; ; i++ {
@@ -294,7 +325,7 @@ func (c *ApiController) UploadResource() {
return return
} }
_, applicationId := util.GetOwnerAndNameFromIdNoCheck(strings.TrimRight(fullFilePath, ".html")) _, applicationId := util.GetOwnerAndNameFromIdNoCheck(strings.TrimSuffix(fullFilePath, ".html"))
applicationObj, err := object.GetApplication(applicationId) applicationObj, err := object.GetApplication(applicationId)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
@@ -307,6 +338,25 @@ func (c *ApiController) UploadResource() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
case "idCardFront", "idCardBack", "idCardWithPerson":
user, err := object.GetUserNoCheck(util.GetId(owner, username))
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(c.T("resource:User is nil for tag: avatar"))
return
}
user.Properties[tag] = fileUrl
user.Properties["isIdCardVerified"] = "false"
_, err = object.UpdateUser(user.GetId(), user, []string{"properties"}, false)
if err != nil {
c.ResponseError(err.Error())
return
}
} }
c.ResponseOk(fileUrl, objectKey) c.ResponseOk(fileUrl, objectKey)

View File

@@ -93,10 +93,9 @@ func (c *ApiController) SendVerificationCode() {
} }
} }
// mfaSessionData != nil, means method is MfaAuthVerification // mfaUserSession != "", means method is MfaAuthVerification
if mfaSessionData := c.getMfaSessionData(); mfaSessionData != nil { if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
user, err = object.GetUser(mfaSessionData.UserId) user, err = object.GetUser(mfaUserSession)
c.setMfaSessionData(nil)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@@ -134,6 +133,8 @@ func (c *ApiController) SendVerificationCode() {
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }
} else if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaDestSession, vform.Dest)
} }
provider, err := application.GetEmailProvider() provider, err := application.GetEmailProvider()
@@ -164,6 +165,11 @@ func (c *ApiController) SendVerificationCode() {
vform.CountryCode = user.GetCountryCode(vform.CountryCode) vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} }
} }
if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaCountryCodeSession, vform.CountryCode)
c.SetSession(object.MfaDestSession, vform.Dest)
}
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false) mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
@@ -187,11 +193,6 @@ func (c *ApiController) SendVerificationCode() {
} }
} }
if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaSmsCountryCodeSession, vform.CountryCode)
c.SetSession(object.MfaSmsDestSession, vform.Dest)
}
if sendResp != nil { if sendResp != nil {
c.ResponseError(sendResp.Error()) c.ResponseError(sendResp.Error())
} else { } else {

View File

@@ -25,6 +25,12 @@ import (
) )
func TestDeployStaticFiles(t *testing.T) { func TestDeployStaticFiles(t *testing.T) {
provider := object.GetProvider(util.GetId("admin", "provider_storage_aliyun_oss")) object.InitConfig()
provider, err := object.GetProvider(util.GetId("admin", "provider_storage_aliyun_oss"))
if err != nil {
panic(err)
}
deployStaticFiles(provider) deployStaticFiles(provider)
} }

1
go.mod
View File

@@ -39,6 +39,7 @@ require (
github.com/lib/pq v1.10.2 github.com/lib/pq v1.10.2
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
github.com/markbates/goth v1.75.2 github.com/markbates/goth v1.75.2
github.com/mitchellh/mapstructure v1.5.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5 github.com/nyaruka/phonenumbers v1.1.5
github.com/pkoukk/tiktoken-go v0.1.1 github.com/pkoukk/tiktoken-go v0.1.1

View File

@@ -20,32 +20,37 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
_ "net/url"
_ "time"
"github.com/casdoor/casdoor/util"
"github.com/mitchellh/mapstructure"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
type CustomIdProvider struct { type CustomIdProvider struct {
Client *http.Client Client *http.Client
Config *oauth2.Config Config *oauth2.Config
UserInfoUrl string
UserInfoURL string
TokenURL string
AuthURL string
UserMapping map[string]string
Scopes []string
} }
func NewCustomIdProvider(clientId string, clientSecret string, redirectUrl string, authUrl string, tokenUrl string, userInfoUrl string) *CustomIdProvider { func NewCustomIdProvider(idpInfo *ProviderInfo, redirectUrl string) *CustomIdProvider {
idp := &CustomIdProvider{} idp := &CustomIdProvider{}
idp.UserInfoUrl = userInfoUrl
config := &oauth2.Config{ idp.Config = &oauth2.Config{
ClientID: clientId, ClientID: idpInfo.ClientId,
ClientSecret: clientSecret, ClientSecret: idpInfo.ClientSecret,
RedirectURL: redirectUrl, RedirectURL: redirectUrl,
Endpoint: oauth2.Endpoint{ Endpoint: oauth2.Endpoint{
AuthURL: authUrl, AuthURL: idpInfo.AuthURL,
TokenURL: tokenUrl, TokenURL: idpInfo.TokenURL,
}, },
} }
idp.Config = config idp.UserInfoURL = idpInfo.UserInfoURL
idp.UserMapping = idpInfo.UserMapping
return idp return idp
} }
@@ -60,22 +65,20 @@ func (idp *CustomIdProvider) GetToken(code string) (*oauth2.Token, error) {
} }
type CustomUserInfo struct { type CustomUserInfo struct {
Id string `json:"sub"` Id string `mapstructure:"id"`
Name string `json:"preferred_username,omitempty"` Username string `mapstructure:"username"`
DisplayName string `json:"name"` DisplayName string `mapstructure:"displayName"`
Email string `json:"email"` Email string `mapstructure:"email"`
AvatarUrl string `json:"picture"` AvatarUrl string `mapstructure:"avatarUrl"`
Status string `json:"status"`
Msg string `json:"msg"`
} }
func (idp *CustomIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { func (idp *CustomIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
ctUserinfo := &CustomUserInfo{}
accessToken := token.AccessToken accessToken := token.AccessToken
request, err := http.NewRequest("GET", idp.UserInfoUrl, nil) request, err := http.NewRequest("GET", idp.UserInfoURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// add accessToken to request header // add accessToken to request header
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
resp, err := idp.Client.Do(request) resp, err := idp.Client.Do(request)
@@ -89,21 +92,40 @@ func (idp *CustomIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
return nil, err return nil, err
} }
err = json.Unmarshal(data, ctUserinfo) var dataMap map[string]interface{}
err = json.Unmarshal(data, &dataMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ctUserinfo.Status != "" { // map user info
return nil, fmt.Errorf("err: %s", ctUserinfo.Msg) for k, v := range idp.UserMapping {
_, ok := dataMap[v]
if !ok {
return nil, fmt.Errorf("cannot find %s in user from castom provider", v)
}
dataMap[k] = dataMap[v]
}
// try to parse id to string
id, err := util.ParseIdToString(dataMap["id"])
if err != nil {
return nil, err
}
dataMap["id"] = id
customUserinfo := &CustomUserInfo{}
err = mapstructure.Decode(dataMap, customUserinfo)
if err != nil {
return nil, err
} }
userInfo := &UserInfo{ userInfo := &UserInfo{
Id: ctUserinfo.Id, Id: customUserinfo.Id,
Username: ctUserinfo.Name, Username: customUserinfo.Username,
DisplayName: ctUserinfo.DisplayName, DisplayName: customUserinfo.DisplayName,
Email: ctUserinfo.Email, Email: customUserinfo.Email,
AvatarUrl: ctUserinfo.AvatarUrl, AvatarUrl: customUserinfo.AvatarUrl,
} }
return userInfo, nil return userInfo, nil
} }

View File

@@ -32,72 +32,89 @@ type UserInfo struct {
AvatarUrl string AvatarUrl string
} }
type ProviderInfo struct {
Type string
SubType string
ClientId string
ClientSecret string
AppId string
HostUrl string
RedirectUrl string
TokenURL string
AuthURL string
UserInfoURL string
UserMapping map[string]string
}
type IdProvider interface { type IdProvider interface {
SetHttpClient(client *http.Client) SetHttpClient(client *http.Client)
GetToken(code string) (*oauth2.Token, error) GetToken(code string) (*oauth2.Token, error)
GetUserInfo(token *oauth2.Token) (*UserInfo, error) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
} }
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string, authUrl string, tokenUrl string, userInfoUrl string) IdProvider { func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) IdProvider {
if typ == "GitHub" { switch idpInfo.Type {
return NewGithubIdProvider(clientId, clientSecret, redirectUrl) case "GitHub":
} else if typ == "Google" { return NewGithubIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGoogleIdProvider(clientId, clientSecret, redirectUrl) case "Google":
} else if typ == "QQ" { return NewGoogleIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewQqIdProvider(clientId, clientSecret, redirectUrl) case "QQ":
} else if typ == "WeChat" { return NewQqIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeChatIdProvider(clientId, clientSecret, redirectUrl) case "WeChat":
} else if typ == "Facebook" { return NewWeChatIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewFacebookIdProvider(clientId, clientSecret, redirectUrl) case "Facebook":
} else if typ == "DingTalk" { return NewFacebookIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewDingTalkIdProvider(clientId, clientSecret, redirectUrl) case "DingTalk":
} else if typ == "Weibo" { return NewDingTalkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeiBoIdProvider(clientId, clientSecret, redirectUrl) case "Weibo":
} else if typ == "Gitee" { return NewWeiBoIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGiteeIdProvider(clientId, clientSecret, redirectUrl) case "Gitee":
} else if typ == "LinkedIn" { return NewGiteeIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewLinkedInIdProvider(clientId, clientSecret, redirectUrl) case "LinkedIn":
} else if typ == "WeCom" { return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
if subType == "Internal" { case "WeCom":
return NewWeComInternalIdProvider(clientId, clientSecret, redirectUrl) if idpInfo.SubType == "Internal" {
} else if subType == "Third-party" { return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeComIdProvider(clientId, clientSecret, redirectUrl) } else if idpInfo.SubType == "Third-party" {
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
} else { } else {
return nil return nil
} }
} else if typ == "Lark" { case "Lark":
return NewLarkIdProvider(clientId, clientSecret, redirectUrl) return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
} else if typ == "GitLab" { case "GitLab":
return NewGitlabIdProvider(clientId, clientSecret, redirectUrl) return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
} else if typ == "Adfs" { case "Adfs":
return NewAdfsIdProvider(clientId, clientSecret, redirectUrl, hostUrl) return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
} else if typ == "Baidu" { case "Baidu":
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl) return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
} else if typ == "Alipay" { case "Alipay":
return NewAlipayIdProvider(clientId, clientSecret, redirectUrl) return NewAlipayIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
} else if typ == "Custom" { case "Custom":
return NewCustomIdProvider(clientId, clientSecret, redirectUrl, authUrl, tokenUrl, userInfoUrl) return NewCustomIdProvider(idpInfo, redirectUrl)
} else if typ == "Infoflow" { case "Infoflow":
if subType == "Internal" { if idpInfo.SubType == "Internal" {
return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl) return NewInfoflowInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl)
} else if subType == "Third-party" { } else if idpInfo.SubType == "Third-party" {
return NewInfoflowIdProvider(clientId, clientSecret, appId, redirectUrl) return NewInfoflowIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl)
} else { } else {
return nil return nil
} }
} else if typ == "Casdoor" { case "Casdoor":
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl) return NewCasdoorIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
} else if typ == "Okta" { case "Okta":
return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl) return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
} else if typ == "Douyin" { case "Douyin":
return NewDouyinIdProvider(clientId, clientSecret, redirectUrl) return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
} else if isGothSupport(typ) { case "Bilibili":
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl, hostUrl) return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
} else if typ == "Bilibili" { default:
return NewBilibiliIdProvider(clientId, clientSecret, redirectUrl) if isGothSupport(idpInfo.Type) {
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
}
return nil
} }
return nil
} }
var gothList = []string{ var gothList = []string{

View File

@@ -61,7 +61,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"}, {Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"}, {Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"}, {Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
@@ -93,7 +93,7 @@ func initBuiltInOrganization() bool {
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", conf.GetConfigString("staticBaseUrl")), Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", conf.GetConfigString("staticBaseUrl")),
PasswordType: "plain", PasswordType: "plain",
PasswordOptions: []string{"AtLeast6"}, PasswordOptions: []string{"AtLeast6"},
CountryCodes: []string{"US", "ES", "CN", "FR", "DE", "GB", "JP", "KR", "VN", "ID", "SG", "IN"}, CountryCodes: []string{"US", "ES", "FR", "DE", "GB", "CN", "JP", "KR", "VN", "ID", "SG", "IN"},
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")), DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Tags: []string{}, Tags: []string{},
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"}, Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"},
@@ -130,7 +130,7 @@ func initBuiltInUser() {
Avatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")), Avatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Email: "admin@example.com", Email: "admin@example.com",
Phone: "12345678910", Phone: "12345678910",
CountryCode: "CN", CountryCode: "US",
Address: []string{}, Address: []string{},
Affiliation: "Example Inc.", Affiliation: "Example Inc.",
Tag: "staff", Tag: "staff",

View File

@@ -103,6 +103,37 @@ func GetLdap(id string) (*Ldap, error) {
} }
} }
func GetMaskedLdap(ldap *Ldap, errs ...error) (*Ldap, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
if ldap == nil {
return nil, nil
}
if ldap.Password != "" {
ldap.Password = "***"
}
return ldap, nil
}
func GetMaskedLdaps(ldaps []*Ldap, errs ...error) ([]*Ldap, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
var err error
for _, ldap := range ldaps {
ldap, err = GetMaskedLdap(ldap)
if err != nil {
return nil, err
}
}
return ldaps, nil
}
func UpdateLdap(ldap *Ldap) (bool, error) { func UpdateLdap(ldap *Ldap) (bool, error) {
if l, err := GetLdap(ldap.Id); err != nil { if l, err := GetLdap(ldap.Id); err != nil {
return false, nil return false, nil

View File

@@ -24,10 +24,6 @@ import (
const MfaRecoveryCodesSession = "mfa_recovery_codes" const MfaRecoveryCodesSession = "mfa_recovery_codes"
type MfaSessionData struct {
UserId string
}
type MfaProps struct { type MfaProps struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"` IsPreferred bool `json:"isPreferred"`

View File

@@ -24,8 +24,8 @@ import (
) )
const ( const (
MfaSmsCountryCodeSession = "mfa_country_code" MfaCountryCodeSession = "mfa_country_code"
MfaSmsDestSession = "mfa_dest" MfaDestSession = "mfa_dest"
) )
type SmsMfa struct { type SmsMfa struct {
@@ -48,9 +48,19 @@ func (mfa *SmsMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, err
} }
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error { func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string) destSession := ctx.Input.CruSession.Get(MfaDestSession)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string) if destSession == nil {
return errors.New("dest session is missing")
}
dest := destSession.(string)
if !util.IsEmailValid(dest) { 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) dest, _ = util.GetE164Number(dest, countryCode)
} }
@@ -78,8 +88,8 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
columns = append(columns, "mfa_phone_enabled") columns = append(columns, "mfa_phone_enabled")
if user.Phone == "" { if user.Phone == "" {
user.Phone = ctx.Input.CruSession.Get(MfaSmsDestSession).(string) user.Phone = ctx.Input.CruSession.Get(MfaDestSession).(string)
user.CountryCode = ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string) user.CountryCode = ctx.Input.CruSession.Get(MfaCountryCodeSession).(string)
columns = append(columns, "phone", "country_code") columns = append(columns, "phone", "country_code")
} }
} else if mfa.Config.MfaType == EmailType { } else if mfa.Config.MfaType == EmailType {
@@ -87,7 +97,7 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
columns = append(columns, "mfa_email_enabled") columns = append(columns, "mfa_email_enabled")
if user.Email == "" { if user.Email == "" {
user.Email = ctx.Input.CruSession.Get(MfaSmsDestSession).(string) user.Email = ctx.Input.CruSession.Get(MfaDestSession).(string)
columns = append(columns, "email") columns = append(columns, "email")
} }
} }
@@ -96,6 +106,11 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
if err != nil { if err != nil {
return err return err
} }
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
ctx.Input.CruSession.Delete(MfaDestSession)
ctx.Input.CruSession.Delete(MfaCountryCodeSession)
return nil return nil
} }

View File

@@ -72,8 +72,11 @@ func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, er
} }
func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error { func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string) secret := ctx.Input.CruSession.Get(MfaTotpSecretSession)
result := totp.Validate(passcode, secret) if secret == nil {
return errors.New("totp secret is missing")
}
result := totp.Validate(passcode, secret.(string))
if result { if result {
return nil return nil
@@ -104,6 +107,10 @@ func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
if err != nil { if err != nil {
return err return err
} }
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
ctx.Input.CruSession.Delete(MfaTotpSecretSession)
return nil return nil
} }

View File

@@ -123,6 +123,10 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
// link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html // link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
// or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key // or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
for _, cert := range certs { for _, cert := range certs {
if cert.Type != "x509" {
continue
}
certPemBlock := []byte(cert.Certificate) certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock) certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes) x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)

View File

@@ -69,7 +69,7 @@ type Organization struct {
IsProfilePublic bool `json:"isProfilePublic"` IsProfilePublic bool `json:"isProfilePublic"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(3000)" json:"accountItems"` AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
} }
func GetOrganizationCount(owner, field, value string) (int64, error) { func GetOrganizationCount(owner, field, value string) (int64, error) {
@@ -476,10 +476,21 @@ func organizationChangeTrigger(oldName string, newName string) error {
return session.Commit() return session.Commit()
} }
func (org *Organization) HasRequiredMfa() bool { func IsNeedPromptMfa(org *Organization, user *User) bool {
if org == nil || user == nil {
return false
}
for _, item := range org.MfaItems { for _, item := range org.MfaItems {
if item.Rule == "Required" { if item.Rule == "Required" {
return true if item.Name == EmailType && !user.MfaEmailEnabled {
return true
}
if item.Name == SmsType && !user.MfaPhoneEnabled {
return true
}
if item.Name == TotpType && user.TotpSecret == "" {
return true
}
} }
} }
return false return false

View File

@@ -16,8 +16,11 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/pp" "github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@@ -28,21 +31,22 @@ type Provider struct {
Name string `xorm:"varchar(100) notnull pk unique" json:"name"` Name string `xorm:"varchar(100) notnull pk unique" 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"`
Category string `xorm:"varchar(100)" json:"category"` Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
SubType string `xorm:"varchar(100)" json:"subType"` SubType string `xorm:"varchar(100)" json:"subType"`
Method string `xorm:"varchar(100)" json:"method"` Method string `xorm:"varchar(100)" json:"method"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"` ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
ClientId2 string `xorm:"varchar(100)" json:"clientId2"` ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"` ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
Cert string `xorm:"varchar(100)" json:"cert"` Cert string `xorm:"varchar(100)" json:"cert"`
CustomAuthUrl string `xorm:"varchar(200)" json:"customAuthUrl"` CustomAuthUrl string `xorm:"varchar(200)" json:"customAuthUrl"`
CustomScope string `xorm:"varchar(200)" json:"customScope"` CustomTokenUrl string `xorm:"varchar(200)" json:"customTokenUrl"`
CustomTokenUrl string `xorm:"varchar(200)" json:"customTokenUrl"` CustomUserInfoUrl string `xorm:"varchar(200)" json:"customUserInfoUrl"`
CustomUserInfoUrl string `xorm:"varchar(200)" json:"customUserInfoUrl"` CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
CustomLogo string `xorm:"varchar(200)" json:"customLogo"` Scopes string `xorm:"varchar(100)" json:"scopes"`
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"`
Host string `xorm:"varchar(100)" json:"host"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `json:"port"`
@@ -365,3 +369,27 @@ func providerChangeTrigger(oldName string, newName string) error {
return session.Commit() return session.Commit()
} }
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.ProviderInfo {
providerInfo := &idp.ProviderInfo{
Type: provider.Type,
SubType: provider.SubType,
ClientId: provider.ClientId,
ClientSecret: provider.ClientSecret,
AppId: provider.AppId,
HostUrl: provider.Host,
TokenURL: provider.CustomTokenUrl,
AuthURL: provider.CustomAuthUrl,
UserInfoURL: provider.CustomUserInfoUrl,
UserMapping: provider.UserMapping,
}
if provider.Type == "WeChat" {
if ctx != nil && strings.Contains(ctx.Request.UserAgent(), "MicroMessenger") {
providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2
}
}
return providerInfo
}

View File

@@ -160,8 +160,8 @@ type User struct {
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"` WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"` PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"` RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret,omitempty"` TotpSecret string `xorm:"varchar(100)" json:"totpSecret"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"` MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"` MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"` MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
@@ -832,11 +832,14 @@ func userChangeTrigger(oldName string, newName string) error {
} }
func (user *User) IsMfaEnabled() bool { func (user *User) IsMfaEnabled() bool {
if user == nil {
return false
}
return user.PreferredMfaType != "" return user.PreferredMfaType != ""
} }
func (user *User) GetPreferredMfaProps(masked bool) *MfaProps { func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
if user.PreferredMfaType == "" { if user == nil || user.PreferredMfaType == "" {
return nil return nil
} }
return user.GetMfaProps(user.PreferredMfaType, masked) return user.GetMfaProps(user.PreferredMfaType, masked)

View File

@@ -293,7 +293,13 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
} }
if oldUser.Groups == nil {
oldUser.Groups = []string{}
}
oldUserGroupsJson, _ := json.Marshal(oldUser.Groups) oldUserGroupsJson, _ := json.Marshal(oldUser.Groups)
if newUser.Groups == nil {
newUser.Groups = []string{}
}
newUserGroupsJson, _ := json.Marshal(newUser.Groups) newUserGroupsJson, _ := json.Marshal(newUser.Groups)
if string(oldUserGroupsJson) != string(newUserGroupsJson) { if string(oldUserGroupsJson) != string(newUserGroupsJson) {
item := GetAccountItemByName("Groups", organization) item := GetAccountItemByName("Groups", organization)

View File

@@ -55,7 +55,7 @@ func StaticFilter(ctx *context.Context) {
path += urlPath path += urlPath
} }
path2 := strings.TrimLeft(path, "web/build/images/") path2 := strings.TrimPrefix(path, "web/build/images/")
if util.FileExist(path2) { if util.FileExist(path2) {
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path2) makeGzipResponse(ctx.ResponseWriter, ctx.Request, path2)
return return

View File

@@ -476,7 +476,26 @@
"tags": [ "tags": [
"Resource API" "Resource API"
], ],
"operationId": "ApiController.AddResource" "operationId": "ApiController.AddResource",
"parameters": [
{
"in": "body",
"name": "resource",
"description": "Resource object",
"required": true,
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
],
"responses": {
"200": {
"description": "Success or error",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
} }
}, },
"/api/add-role": { "/api/add-role": {
@@ -1344,7 +1363,26 @@
"tags": [ "tags": [
"Resource API" "Resource API"
], ],
"operationId": "ApiController.DeleteResource" "operationId": "ApiController.DeleteResource",
"parameters": [
{
"in": "body",
"name": "resource",
"description": "Resource object",
"required": true,
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
],
"responses": {
"200": {
"description": "Success or error",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
} }
}, },
"/api/delete-role": { "/api/delete-role": {
@@ -2797,7 +2835,25 @@
"tags": [ "tags": [
"Resource API" "Resource API"
], ],
"operationId": "ApiController.GetResource" "description": "get resource",
"operationId": "ApiController.GetResource",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of resource",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
}
} }
}, },
"/api/get-resources": { "/api/get-resources": {
@@ -2805,7 +2861,71 @@
"tags": [ "tags": [
"Resource API" "Resource API"
], ],
"operationId": "ApiController.GetResources" "description": "get resources",
"operationId": "ApiController.GetResources",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "Owner",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "user",
"description": "User",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "pageSize",
"description": "Page Size",
"type": "integer"
},
{
"in": "query",
"name": "p",
"description": "Page Number",
"type": "integer"
},
{
"in": "query",
"name": "field",
"description": "Field",
"type": "string"
},
{
"in": "query",
"name": "value",
"description": "Value",
"type": "string"
},
{
"in": "query",
"name": "sortField",
"description": "Sort Field",
"type": "string"
},
{
"in": "query",
"name": "sortOrder",
"description": "Sort Order",
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Resource"
}
}
}
}
} }
}, },
"/api/get-role": { "/api/get-role": {
@@ -4532,7 +4652,34 @@
"tags": [ "tags": [
"Resource API" "Resource API"
], ],
"operationId": "ApiController.UpdateResource" "description": "get resource",
"operationId": "ApiController.UpdateResource",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of resource",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "resource",
"description": "The resource object",
"required": true,
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
],
"responses": {
"200": {
"description": "Success or error",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
} }
}, },
"/api/update-role": { "/api/update-role": {
@@ -4779,7 +4926,76 @@
"tags": [ "tags": [
"Resource API" "Resource API"
], ],
"operationId": "ApiController.UploadResource" "operationId": "ApiController.UploadResource",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "Owner",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "user",
"description": "User",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "application",
"description": "Application",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "tag",
"description": "Tag",
"type": "string"
},
{
"in": "query",
"name": "parent",
"description": "Parent",
"type": "string"
},
{
"in": "query",
"name": "fullFilePath",
"description": "Full File Path",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "createdTime",
"description": "Created Time",
"type": "string"
},
{
"in": "query",
"name": "description",
"description": "Description",
"type": "string"
},
{
"in": "formData",
"name": "file",
"description": "Resource file",
"required": true,
"type": "file"
}
],
"responses": {
"200": {
"description": "FileUrl, objectKey",
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
}
} }
}, },
"/api/user": { "/api/user": {
@@ -4974,13 +5190,13 @@
"properties": { "properties": {
"data": { "data": {
"additionalProperties": { "additionalProperties": {
"description": "support string | class | List\u003cclass\u003e and os on", "description": "support string, struct or []struct",
"type": "string" "type": "string"
} }
}, },
"data2": { "data2": {
"additionalProperties": { "additionalProperties": {
"description": "support string | class | List\u003cclass\u003e and os on", "description": "support string, struct or []struct",
"type": "string" "type": "string"
} }
}, },
@@ -5196,6 +5412,12 @@
"signupUrl": { "signupUrl": {
"type": "string" "type": "string"
}, },
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"termsOfUse": { "termsOfUse": {
"type": "string" "type": "string"
}, },
@@ -6126,9 +6348,6 @@
"customLogo": { "customLogo": {
"type": "string" "type": "string"
}, },
"customScope": {
"type": "string"
},
"customTokenUrl": { "customTokenUrl": {
"type": "string" "type": "string"
}, },
@@ -6190,6 +6409,9 @@
"regionId": { "regionId": {
"type": "string" "type": "string"
}, },
"scopes": {
"type": "string"
},
"signName": { "signName": {
"type": "string" "type": "string"
}, },
@@ -6204,6 +6426,11 @@
}, },
"type": { "type": {
"type": "string" "type": "string"
},
"userMapping": {
"additionalProperties": {
"type": "string"
}
} }
} }
}, },
@@ -6286,6 +6513,55 @@
} }
} }
}, },
"object.Resource": {
"title": "Resource",
"type": "object",
"properties": {
"application": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"description": {
"type": "string"
},
"fileFormat": {
"type": "string"
},
"fileName": {
"type": "string"
},
"fileSize": {
"type": "integer",
"format": "int64"
},
"fileType": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"parent": {
"type": "string"
},
"provider": {
"type": "string"
},
"tag": {
"type": "string"
},
"url": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"object.Role": { "object.Role": {
"title": "Role", "title": "Role",
"type": "object", "type": "object",

View File

@@ -308,6 +308,18 @@ paths:
tags: tags:
- Resource API - Resource API
operationId: ApiController.AddResource operationId: ApiController.AddResource
parameters:
- in: body
name: resource
description: Resource object
required: true
schema:
$ref: '#/definitions/object.Resource'
responses:
"200":
description: Success or error
schema:
$ref: '#/definitions/controllers.Response'
/api/add-role: /api/add-role:
post: post:
tags: tags:
@@ -869,6 +881,18 @@ paths:
tags: tags:
- Resource API - Resource API
operationId: ApiController.DeleteResource operationId: ApiController.DeleteResource
parameters:
- in: body
name: resource
description: Resource object
required: true
schema:
$ref: '#/definitions/object.Resource'
responses:
"200":
description: Success or error
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-role: /api/delete-role:
post: post:
tags: tags:
@@ -1818,12 +1842,67 @@ paths:
get: get:
tags: tags:
- Resource API - Resource API
description: get resource
operationId: ApiController.GetResource operationId: ApiController.GetResource
parameters:
- in: query
name: id
description: The id ( owner/name ) of resource
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Resource'
/api/get-resources: /api/get-resources:
get: get:
tags: tags:
- Resource API - Resource API
description: get resources
operationId: ApiController.GetResources operationId: ApiController.GetResources
parameters:
- in: query
name: owner
description: Owner
required: true
type: string
- in: query
name: user
description: User
required: true
type: string
- in: query
name: pageSize
description: Page Size
type: integer
- in: query
name: p
description: Page Number
type: integer
- in: query
name: field
description: Field
type: string
- in: query
name: value
description: Value
type: string
- in: query
name: sortField
description: Sort Field
type: string
- in: query
name: sortOrder
description: Sort Order
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Resource'
/api/get-role: /api/get-role:
get: get:
tags: tags:
@@ -2960,7 +3039,25 @@ paths:
post: post:
tags: tags:
- Resource API - Resource API
description: get resource
operationId: ApiController.UpdateResource operationId: ApiController.UpdateResource
parameters:
- in: query
name: id
description: The id ( owner/name ) of resource
required: true
type: string
- in: body
name: resource
description: The resource object
required: true
schema:
$ref: '#/definitions/object.Resource'
responses:
"200":
description: Success or error
schema:
$ref: '#/definitions/controllers.Response'
/api/update-role: /api/update-role:
post: post:
tags: tags:
@@ -3123,6 +3220,53 @@ paths:
tags: tags:
- Resource API - Resource API
operationId: ApiController.UploadResource operationId: ApiController.UploadResource
parameters:
- in: query
name: owner
description: Owner
required: true
type: string
- in: query
name: user
description: User
required: true
type: string
- in: query
name: application
description: Application
required: true
type: string
- in: query
name: tag
description: Tag
type: string
- in: query
name: parent
description: Parent
type: string
- in: query
name: fullFilePath
description: Full File Path
required: true
type: string
- in: query
name: createdTime
description: Created Time
type: string
- in: query
name: description
description: Description
type: string
- in: formData
name: file
description: Resource file
required: true
type: file
responses:
"200":
description: FileUrl, objectKey
schema:
$ref: '#/definitions/object.Resource'
/api/user: /api/user:
get: get:
tags: tags:
@@ -3251,11 +3395,11 @@ definitions:
properties: properties:
data: data:
additionalProperties: additionalProperties:
description: support string | class | List<class> and os on description: support string, struct or []struct
type: string type: string
data2: data2:
additionalProperties: additionalProperties:
description: support string | class | List<class> and os on description: support string, struct or []struct
type: string type: string
msg: msg:
type: string type: string
@@ -3400,6 +3544,10 @@ definitions:
$ref: '#/definitions/object.SignupItem' $ref: '#/definitions/object.SignupItem'
signupUrl: signupUrl:
type: string type: string
tags:
type: array
items:
type: string
termsOfUse: termsOfUse:
type: string type: string
themeData: themeData:
@@ -4026,8 +4174,6 @@ definitions:
type: string type: string
customLogo: customLogo:
type: string type: string
customScope:
type: string
customTokenUrl: customTokenUrl:
type: string type: string
customUserInfoUrl: customUserInfoUrl:
@@ -4069,6 +4215,8 @@ definitions:
type: string type: string
regionId: regionId:
type: string type: string
scopes:
type: string
signName: signName:
type: string type: string
subType: subType:
@@ -4079,6 +4227,9 @@ definitions:
type: string type: string
type: type:
type: string type: string
userMapping:
additionalProperties:
type: string
object.ProviderItem: object.ProviderItem:
title: ProviderItem title: ProviderItem
type: object type: object
@@ -4132,6 +4283,39 @@ definitions:
type: string type: string
user: user:
type: string type: string
object.Resource:
title: Resource
type: object
properties:
application:
type: string
createdTime:
type: string
description:
type: string
fileFormat:
type: string
fileName:
type: string
fileSize:
type: integer
format: int64
fileType:
type: string
name:
type: string
owner:
type: string
parent:
type: string
provider:
type: string
tag:
type: string
url:
type: string
user:
type: string
object.Role: object.Role:
title: Role title: Role
type: object type: object

View File

@@ -289,3 +289,18 @@ func HasString(strs []string, str string) bool {
} }
return false return false
} }
func ParseIdToString(input interface{}) (string, error) {
switch v := input.(type) {
case string:
return v, nil
case int:
return strconv.Itoa(v), nil
case int64:
return strconv.FormatInt(v, 10), nil
case float64:
return strconv.FormatFloat(v, 'f', -1, 64), nil
default:
return "", fmt.Errorf("unsupported id type: %T", input)
}
}

View File

@@ -246,3 +246,23 @@ func TestSnakeString(t *testing.T) {
}) })
} }
} }
func TestParseId(t *testing.T) {
scenarios := []struct {
description string
input interface{}
expected interface{}
}{
{"Should be return 123456", "123456", "123456"},
{"Should be return 123456", 123456, "123456"},
{"Should be return 123456", int64(123456), "123456"},
{"Should be return 123456", float64(123456), "123456"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual, err := ParseIdToString(scenery.input)
assert.Nil(t, err, "The returned value not is expected")
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}

View File

@@ -15,9 +15,11 @@
import React, {Component} from "react"; import React, {Component} from "react";
import "./App.less"; import "./App.less";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import EnableMfaNotification from "./common/notifaction/EnableMfaNotification";
import GroupTreePage from "./GroupTreePage"; import GroupTreePage from "./GroupTreePage";
import GroupEditPage from "./GroupEdit"; import GroupEditPage from "./GroupEdit";
import GroupListPage from "./GroupList"; import GroupListPage from "./GroupList";
import {MfaRuleRequired} from "./Setting";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {BarsOutlined, CommentOutlined, DownOutlined, InfoCircleFilled, LogoutOutlined, SettingOutlined} from "@ant-design/icons"; import {BarsOutlined, CommentOutlined, DownOutlined, InfoCircleFilled, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
@@ -64,6 +66,13 @@ import ProductBuyPage from "./ProductBuyPage";
import PaymentListPage from "./PaymentListPage"; import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage"; import PaymentEditPage from "./PaymentEditPage";
import PaymentResultPage from "./PaymentResultPage"; import PaymentResultPage from "./PaymentResultPage";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
import SessionListPage from "./SessionListPage";
import MfaSetupPage from "./auth/MfaSetupPage";
import SystemInfo from "./SystemInfo";
import AccountPage from "./account/AccountPage"; import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage"; import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./common/CustomGithubCorner"; import CustomGithubCorner from "./common/CustomGithubCorner";
@@ -73,19 +82,12 @@ import * as Auth from "./auth/Auth";
import EntryPage from "./EntryPage"; import EntryPage from "./EntryPage";
import * as AuthBackend from "./auth/AuthBackend"; import * as AuthBackend from "./auth/AuthBackend";
import AuthCallback from "./auth/AuthCallback"; import AuthCallback from "./auth/AuthCallback";
import LanguageSelect from "./common/select/LanguageSelect";
import i18next from "i18next";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage"; import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from "./auth/SamlCallback"; import SamlCallback from "./auth/SamlCallback";
import ModelListPage from "./ModelListPage"; import i18next from "i18next";
import ModelEditPage from "./ModelEditPage";
import SystemInfo from "./SystemInfo";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
import {withTranslation} from "react-i18next"; import {withTranslation} from "react-i18next";
import LanguageSelect from "./common/select/LanguageSelect";
import ThemeSelect from "./common/select/ThemeSelect"; import ThemeSelect from "./common/select/ThemeSelect";
import SessionListPage from "./SessionListPage";
import MfaSetupPage from "./auth/MfaSetupPage";
import OrganizationSelect from "./common/select/OrganizationSelect"; import OrganizationSelect from "./common/select/OrganizationSelect";
const {Header, Footer, Content} = Layout; const {Header, Footer, Content} = Layout;
@@ -102,6 +104,7 @@ class App extends Component {
themeAlgorithm: ["default"], themeAlgorithm: ["default"],
themeData: Conf.ThemeDefault, themeData: Conf.ThemeDefault,
logo: this.getLogo(Setting.getAlgorithmNames(Conf.ThemeDefault)), logo: this.getLogo(Setting.getAlgorithmNames(Conf.ThemeDefault)),
requiredEnableMfa: false,
}; };
Setting.initServerUrl(); Setting.initServerUrl();
@@ -116,16 +119,29 @@ class App extends Component {
this.getAccount(); this.getAccount();
} }
componentDidUpdate() { componentDidUpdate(prevProps, prevState, snapshot) {
// eslint-disable-next-line no-restricted-globals
const uri = location.pathname; const uri = location.pathname;
if (this.state.uri !== uri) { if (this.state.uri !== uri) {
this.updateMenuKey(); this.updateMenuKey();
} }
if (this.state.account !== prevState.account) {
const requiredEnableMfa = Setting.isRequiredEnableMfa(this.state.account, this.state.account?.organization);
this.setState({
requiredEnableMfa: requiredEnableMfa,
});
if (requiredEnableMfa === true) {
const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [MfaRuleRequired])
.find((item) => item.rule === MfaRuleRequired)?.name;
if (mfaType !== undefined) {
this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, {from: "/login"});
}
}
}
} }
updateMenuKey() { updateMenuKey() {
// eslint-disable-next-line no-restricted-globals
const uri = location.pathname; const uri = location.pathname;
this.setState({ this.setState({
uri: uri, uri: uri,
@@ -341,13 +357,15 @@ class App extends Component {
renderRightDropdown() { renderRightDropdown() {
const items = []; const items = [];
items.push(Setting.getItem(<><SettingOutlined />&nbsp;&nbsp;{i18next.t("account:My Account")}</>, if (this.state.requiredEnableMfa === false) {
"/account" items.push(Setting.getItem(<><SettingOutlined />&nbsp;&nbsp;{i18next.t("account:My Account")}</>,
)); "/account"
if (Conf.EnableChatPages) {
items.push(Setting.getItem(<><CommentOutlined />&nbsp;&nbsp;{i18next.t("account:Chats & Messages")}</>,
"/chat"
)); ));
if (Conf.EnableChatPages) {
items.push(Setting.getItem(<><CommentOutlined />&nbsp;&nbsp;{i18next.t("account:Chats & Messages")}</>,
"/chat"
));
}
} }
items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Logout")}</>, items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Logout")}</>,
"/logout")); "/logout"));
@@ -547,14 +565,6 @@ class App extends Component {
return res; return res;
} }
renderHomeIfLoggedIn(component) {
if (this.state.account !== null && this.state.account !== undefined) {
return <Redirect to="/" />;
} else {
return component;
}
}
renderLoginIfNotLoggedIn(component) { renderLoginIfNotLoggedIn(component) {
if (this.state.account === null) { if (this.state.account === null) {
sessionStorage.setItem("from", window.location.pathname); sessionStorage.setItem("from", window.location.pathname);
@@ -566,12 +576,6 @@ class App extends Component {
} }
} }
isStartPages() {
return window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/signup") ||
window.location.pathname === "/";
}
renderRouter() { renderRouter() {
return ( return (
<Switch> <Switch>
@@ -629,7 +633,7 @@ class App extends Component {
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} /> <Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} /> <Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} /> <Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/mfa-authentication/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} {...props} />)} /> <Route exact path="/mfa/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} onfinish={() => this.setState({requiredEnableMfa: false})} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} /> <Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} /> <Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")} <Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
@@ -660,19 +664,24 @@ class App extends Component {
if (key === "/swagger") { if (key === "/swagger") {
window.open(Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger", "_blank"); window.open(Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger", "_blank");
} else { } else {
this.props.history.push(key); if (this.state.requiredEnableMfa) {
Setting.showMessage("info", "Please enable MFA first!");
} else {
this.props.history.push(key);
}
} }
}; };
const menuStyleRight = Setting.isAdminUser(this.state.account) && !Setting.isMobile() ? "calc(180px + 260px)" : "260px"; const menuStyleRight = Setting.isAdminUser(this.state.account) && !Setting.isMobile() ? "calc(180px + 260px)" : "260px";
return ( return (
<Layout id="parent-area"> <Layout id="parent-area">
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm.includes("dark") ? "black" : "white"}}> <EnableMfaNotification account={this.state.account} />
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm.includes("dark") ? "black" : "white"}} >
{Setting.isMobile() ? null : ( {Setting.isMobile() ? null : (
<Link to={"/"}> <Link to={"/"}>
<div className="logo" style={{background: `url(${this.state.logo})`}} /> <div className="logo" style={{background: `url(${this.state.logo})`}} />
</Link> </Link>
)} )}
{Setting.isMobile() ? {this.state.requiredEnableMfa || (Setting.isMobile() ?
<React.Fragment> <React.Fragment>
<Drawer title={i18next.t("general:Close")} placement="left" visible={this.state.menuVisible} onClose={this.onClose}> <Drawer title={i18next.t("general:Close")} placement="left" visible={this.state.menuVisible} onClose={this.onClose}>
<Menu <Menu
@@ -695,7 +704,7 @@ class App extends Component {
selectedKeys={[this.state.selectedMenuKey]} selectedKeys={[this.state.selectedMenuKey]}
style={{position: "absolute", left: "145px", right: menuStyleRight}} style={{position: "absolute", left: "145px", right: menuStyleRight}}
/> />
} )}
{ {
this.renderAccountMenu() this.renderAccountMenu()
} }
@@ -759,9 +768,11 @@ class App extends Component {
<EntryPage <EntryPage
account={this.state.account} account={this.state.account}
theme={this.state.themeData} theme={this.state.themeData}
onUpdateAccount={(account) => { onLoginSuccess={(redirectUrl) => {
this.onUpdateAccount(account); localStorage.setItem("mfaRedirectUrl", redirectUrl);
this.getAccount();
}} }}
onUpdateAccount={(account) => this.onUpdateAccount(account)}
updataThemeData={this.setTheme} updataThemeData={this.setTheme}
/> : /> :
<Switch> <Switch>

View File

@@ -158,6 +158,7 @@ class CertEditPage extends React.Component {
{ {
[ [
{id: "x509", name: "x509"}, {id: "x509", name: "x509"},
{id: "Payment", name: "Payment"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>

View File

@@ -151,6 +151,7 @@ class CertListPage extends BaseListPage {
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: "x509", value: "x509"}, {text: "x509", value: "x509"},
{text: "Payment", value: "Payment"},
], ],
width: "110px", width: "110px",
sorter: true, sorter: true,
@@ -213,7 +214,7 @@ class CertListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={certs} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={certs} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -41,7 +41,7 @@ class EntryPage extends React.Component {
renderHomeIfLoggedIn(component) { renderHomeIfLoggedIn(component) {
if (this.props.account !== null && this.props.account !== undefined) { if (this.props.account !== null && this.props.account !== undefined) {
return <Redirect to="/" />; return <Redirect to={{pathname: "/", state: {from: "/login"}}} />;
} else { } else {
return component; return component;
} }

View File

@@ -35,7 +35,7 @@ class OrganizationListPage extends BaseListPage {
passwordType: "plain", passwordType: "plain",
PasswordSalt: "", PasswordSalt: "",
passwordOptions: [], passwordOptions: [],
countryCodes: ["CN"], countryCodes: ["US"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`, defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "", defaultApplication: "",
tags: [], tags: [],
@@ -53,25 +53,40 @@ class OrganizationListPage extends BaseListPage {
{name: "Password", visible: true, viewRule: "Self", modifyRule: "Self"}, {name: "Password", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Email", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Email", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Phone", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Phone", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Country code", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Country/Region", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Country/Region", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Location", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Location", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Address", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Affiliation", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Affiliation", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Title", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Title", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "ID card type", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "ID card", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "ID card info", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Homepage", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Homepage", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Language", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Gender", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Birthday", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Education", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Score", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Karma", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Ranking", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "API key", label: i18next.t("general:API key")}, {name: "API key", label: i18next.t("general:API key"), modifyRule: "Self"},
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"}, {name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"}, {name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"}, {name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"}, {name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is online", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is global admin", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is global admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
], ],
}; };
} }

View File

@@ -196,7 +196,7 @@ class PlanListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={plans} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={plans} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Plans")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Plans")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -165,7 +165,7 @@ class PricingListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={pricings} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={pricings} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Pricings")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Pricings")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -56,8 +56,10 @@ class ProviderEditPage extends React.Component {
} }
if (res.status === "ok") { if (res.status === "ok") {
const provider = res.data;
provider.userMapping = provider.userMapping || {};
this.setState({ this.setState({
provider: res.data, provider: provider,
}); });
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
@@ -93,6 +95,40 @@ class ProviderEditPage extends React.Component {
}); });
} }
updateUserMappingField(key, value) {
const provider = this.state.provider;
provider.userMapping[key] = value;
this.setState({
provider: provider,
});
}
renderUserMappingInput() {
return (
<React.Fragment>
{Setting.getLabel(i18next.t("general:ID"), i18next.t("general:ID - Tooltip"))} :
<Input value={this.state.provider.userMapping.id} onChange={e => {
this.updateUserMappingField("id", e.target.value);
}} />
{Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"))} :
<Input value={this.state.provider.userMapping.username} onChange={e => {
this.updateUserMappingField("username", e.target.value);
}} />
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
<Input value={this.state.provider.userMapping.displayName} onChange={e => {
this.updateUserMappingField("displayName", e.target.value);
}} />
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
<Input value={this.state.provider.userMapping.email} onChange={e => {
this.updateUserMappingField("email", e.target.value);
}} />
{Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} :
<Input value={this.state.provider.userMapping.avatarUrl} onChange={e => {
this.updateUserMappingField("avatarUrl", e.target.value);
}} />
</React.Fragment>
);
}
getClientIdLabel(provider) { getClientIdLabel(provider) {
switch (provider.category) { switch (provider.category) {
case "Email": case "Email":
@@ -350,7 +386,7 @@ class ProviderEditPage extends React.Component {
} }
if (value === "Custom") { if (value === "Custom") {
this.updateProviderField("customAuthUrl", "https://door.casdoor.com/login/oauth/authorize"); this.updateProviderField("customAuthUrl", "https://door.casdoor.com/login/oauth/authorize");
this.updateProviderField("customScope", "openid profile email"); this.updateProviderField("scopes", "openid profile email");
this.updateProviderField("customTokenUrl", "https://door.casdoor.com/api/login/oauth/access_token"); this.updateProviderField("customTokenUrl", "https://door.casdoor.com/api/login/oauth/access_token");
this.updateProviderField("customUserInfoUrl", "https://door.casdoor.com/api/userinfo"); this.updateProviderField("customUserInfoUrl", "https://door.casdoor.com/api/userinfo");
} }
@@ -416,16 +452,6 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.customScope} onChange={e => {
this.updateProviderField("customScope", 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("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))} {Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))}
@@ -436,6 +462,16 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.scopes} onChange={e => {
this.updateProviderField("scopes", 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("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))} {Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))}
@@ -446,6 +482,14 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:User mapping"), i18next.t("provider:User mapping - Tooltip"))} :
</Col>
<Col span={22} >
{this.renderUserMappingInput()}
</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:Favicon"), i18next.t("general:Favicon - Tooltip"))} : {Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :

View File

@@ -34,10 +34,10 @@ export const ServerUrl = "";
export const StaticBaseUrl = "https://cdn.casbin.org"; export const StaticBaseUrl = "https://cdn.casbin.org";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"}, export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "中文", key: "zh", country: "CN", alt: "中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"}, {label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"}, {label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"}, {label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "中文", key: "zh", country: "CN", alt: "中文"},
{label: "Indonesia", key: "id", country: "ID", alt: "Indonesia"}, {label: "Indonesia", key: "id", country: "ID", alt: "Indonesia"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"}, {label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"}, {label: "한국어", key: "ko", country: "KR", alt: "한국어"},
@@ -482,6 +482,26 @@ export function isPromptAnswered(user, application) {
return true; return true;
} }
export const MfaRuleRequired = "Required";
export const MfaRulePrompted = "Prompted";
export const MfaRuleOptional = "Optional";
export function isRequiredEnableMfa(user, organization) {
if (!user || !organization || !organization.mfaItems) {
return false;
}
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
}
export function getMfaItemsByRules(user, organization, mfaRules = []) {
if (!user || !organization || !organization.mfaItems) {
return [];
}
return organization.mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
.filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
}
export function parseObject(s) { export function parseObject(s) {
try { try {
return eval("(" + s + ")"); return eval("(" + s + ")");

View File

@@ -215,7 +215,7 @@ class SubscriptionListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={subscriptions} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={subscriptions} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Subscriptions")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Subscriptions")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -14,6 +14,7 @@
import React from "react"; import React from "react";
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd"; import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd";
import {withRouter} from "react-router-dom";
import * as GroupBackend from "./backend/GroupBackend"; import * as GroupBackend from "./backend/GroupBackend";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
@@ -53,6 +54,7 @@ class UserEditPage extends React.Component {
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
loading: true, loading: true,
returnUrl: null, returnUrl: null,
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
}; };
} }
@@ -269,6 +271,12 @@ class UserEditPage extends React.Component {
} }
} }
if (accountItem.name === "ID card info" || accountItem.name === "ID card") {
if (this.state.user.properties?.isIdCardVerified === "true") {
disabled = true;
}
}
let isKeysGenerated = false; let isKeysGenerated = false;
if (this.state.user.accessKey !== "" && this.state.user.accessKey !== "") { if (this.state.user.accessKey !== "" && this.state.user.accessKey !== "") {
isKeysGenerated = true; isKeysGenerated = true;
@@ -365,20 +373,11 @@ class UserEditPage extends React.Component {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} : {Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
<Row style={{marginTop: "20px"}} > {i18next.t("general:Preview")}:
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> </Col>
{i18next.t("general:Preview")}: <Col>
</Col> {this.renderImage(this.state.user.avatar, i18next.t("user:Upload a photo"), i18next.t("user:Set new profile picture"), "avatar", false)}
<Col span={22} >
<a target="_blank" rel="noreferrer" href={this.state.user.avatar}>
<img src={this.state.user.avatar} alt={this.state.user.avatar} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<CropperDivModal buttonText={`${i18next.t("user:Upload a photo")}...`} title={i18next.t("user:Upload a photo")} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />
</Row>
</Col> </Col>
</Row> </Row>
); );
@@ -536,12 +535,36 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:ID card"), i18next.t("user:ID card - Tooltip"))} : {Setting.getLabel(i18next.t("user:ID card"), i18next.t("user:ID card - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.user.idCard} onChange={e => { <Input value={this.state.user.idCard} disabled={disabled} onChange={e => {
this.updateUserField("idCard", e.target.value); this.updateUserField("idCard", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "ID card info") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:ID card info"), i18next.t("user:ID card info - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Preview")}:
</Col>
{
[
{name: "ID card front", value: "idCardFront"},
{name: "ID card back", value: "idCardBack"},
{name: "ID card with person", value: "idCardWithPerson"},
].map((entry) => {
return this.renderImage(this.state.user.properties[entry.value] || "", this.getIdCardType(entry.name), this.getIdCardText(entry.name), entry.value, disabled);
})
}
</Row>
</Col>
</Row>
);
} else if (accountItem.name === "Homepage") { } else if (accountItem.name === "Homepage") {
return ( return (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@@ -900,7 +923,7 @@ class UserEditPage extends React.Component {
} }
</Space> </Space>
) : <Button type={"default"} onClick={() => { ) : <Button type={"default"} onClick={() => {
Setting.goToLink(`/mfa-authentication/setup?mfaType=${item.mfaType}`); this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
}}> }}>
{i18next.t("mfa:Setup")} {i18next.t("mfa:Setup")}
</Button>} </Button>}
@@ -942,6 +965,25 @@ class UserEditPage extends React.Component {
} }
} }
renderImage(imgUrl, title, set, tag, disabled) {
return (
<Col span={4} style={{textAlign: "center", margin: "auto"}} key={tag}>
{
imgUrl ?
<a target="_blank" rel="noreferrer" href={imgUrl} style={{marginBottom: "10px"}}>
<img src={imgUrl} alt={imgUrl} height={90} style={{marginBottom: "20px"}} />
</a>
:
<Col style={{height: "78%", border: "1px dotted grey", borderRadius: 3, marginBottom: 5}}>
<div style={{fontSize: 30, margin: 10}}>+</div>
<div style={{verticalAlign: "middle", marginBottom: 10}}>{`请上传${title}...`}</div>
</Col>
}
<CropperDivModal disabled={disabled} tag={tag} setTitle={set} buttonText={`${title}...`} title={title} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />
</Col>
);
}
renderUser() { renderUser() {
return ( return (
<Card size="small" title={ <Card size="small" title={
@@ -967,6 +1009,30 @@ class UserEditPage extends React.Component {
); );
} }
getIdCardType(key) {
if (key === "ID card front") {
return i18next.t("user:ID card front");
} else if (key === "ID card back") {
return i18next.t("user:ID card back");
} else if (key === "ID card with person") {
return i18next.t("user:ID card with person");
} else {
return "Unknown Id card name: " + key;
}
}
getIdCardText(key) {
if (key === "ID card front") {
return i18next.t("user:Upload ID card front picture");
} else if (key === "ID card back") {
return i18next.t("user:Upload ID card back picture");
} else if (key === "ID card with person") {
return i18next.t("user:Upload ID card with person picture");
} else {
return "Unknown Id card name: " + key;
}
}
submitUserEdit(needExit) { submitUserEdit(needExit) {
const user = Setting.deepCopy(this.state.user); const user = Setting.deepCopy(this.state.user);
UserBackend.updateUser(this.state.organizationName, this.state.userName, user) UserBackend.updateUser(this.state.organizationName, this.state.userName, user)
@@ -1053,4 +1119,4 @@ class UserEditPage extends React.Component {
} }
} }
export default UserEditPage; export default withRouter(UserEditPage);

View File

@@ -15,6 +15,7 @@
import React from "react"; import React from "react";
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd"; import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd";
import {ArrowLeftOutlined, LockOutlined, UserOutlined} from "@ant-design/icons"; import {ArrowLeftOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
import {withRouter} from "react-router-dom";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend"; import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
import OrganizationSelect from "../common/select/OrganizationSelect"; import OrganizationSelect from "../common/select/OrganizationSelect";
import * as Conf from "../Conf"; import * as Conf from "../Conf";
@@ -34,7 +35,7 @@ import LanguageSelect from "../common/select/LanguageSelect";
import {CaptchaModal} from "../common/modal/CaptchaModal"; import {CaptchaModal} from "../common/modal/CaptchaModal";
import {CaptchaRule} from "../common/modal/CaptchaModal"; import {CaptchaRule} from "../common/modal/CaptchaModal";
import RedirectForm from "../common/RedirectForm"; import RedirectForm from "../common/RedirectForm";
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./MfaAuthVerifyForm"; import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
class LoginPage extends React.Component { class LoginPage extends React.Component {
constructor(props) { constructor(props) {
@@ -81,6 +82,10 @@ class LoginPage extends React.Component {
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if (prevState.loginMethod === undefined && this.state.loginMethod === undefined) {
const application = this.getApplicationObj();
this.setState({loginMethod: this.getDefaultLoginMethod(application)});
}
if (prevProps.application !== this.props.application) { if (prevProps.application !== this.props.application) {
this.setState({loginMethod: this.getDefaultLoginMethod(this.props.application)}); this.setState({loginMethod: this.getDefaultLoginMethod(this.props.application)});
@@ -254,8 +259,13 @@ class LoginPage extends React.Component {
const code = resp.data; const code = resp.data;
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?"; const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
const noRedirect = oAuthParams.noRedirect; const noRedirect = oAuthParams.noRedirect;
const redirectUrl = `${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`;
if (resp.data === RequiredMfa) {
this.props.onLoginSuccess(window.location.href);
return;
}
if (Setting.hasPromptPage(application) || resp.msg === RequiredMfa) { if (Setting.hasPromptPage(application)) {
AuthBackend.getAccount() AuthBackend.getAccount()
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
@@ -263,13 +273,8 @@ class LoginPage extends React.Component {
account.organization = res.data2; account.organization = res.data2;
this.onUpdateAccount(account); this.onUpdateAccount(account);
if (resp.msg === RequiredMfa) {
Setting.goToLink(`/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}&promptType=mfa`);
return;
}
if (Setting.isPromptAnswered(account, application)) { if (Setting.isPromptAnswered(account, application)) {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`); Setting.goToLink(redirectUrl);
} else { } else {
Setting.goToLinkSoft(ths, `/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}`); Setting.goToLinkSoft(ths, `/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}`);
} }
@@ -280,7 +285,7 @@ class LoginPage extends React.Component {
} else { } else {
if (noRedirect === "true") { if (noRedirect === "true") {
window.close(); window.close();
const newWindow = window.open(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`); const newWindow = window.open(redirectUrl);
if (newWindow) { if (newWindow) {
setInterval(() => { setInterval(() => {
if (!newWindow.closed) { if (!newWindow.closed) {
@@ -289,7 +294,7 @@ class LoginPage extends React.Component {
}, 1000); }, 1000);
} }
} else { } else {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`); Setting.goToLink(redirectUrl);
this.sendPopupData({type: "loginSuccess", data: {code: code, state: oAuthParams.state}}, oAuthParams.redirectUri); this.sendPopupData({type: "loginSuccess", data: {code: code, state: oAuthParams.state}}, oAuthParams.redirectUri);
} }
} }
@@ -355,20 +360,8 @@ class LoginPage extends React.Component {
const responseType = values["type"]; const responseType = values["type"];
if (responseType === "login") { if (responseType === "login") {
if (res.msg === RequiredMfa) { Setting.showMessage("success", i18next.t("application:Logged in successfully"));
AuthBackend.getAccount().then((res) => { this.props.onLoginSuccess();
if (res.status === "ok") {
const account = res.data;
account.organization = res.data2;
this.onUpdateAccount(account);
}
});
Setting.goToLink(`/prompt/${this.getApplicationObj().name}?promptType=mfa`);
} else {
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
const link = Setting.getFromLink();
Setting.goToLink(link);
}
} else if (responseType === "code") { } else if (responseType === "code") {
this.postCodeLoginAction(res); this.postCodeLoginAction(res);
} else if (responseType === "token" || responseType === "id_token") { } else if (responseType === "token" || responseType === "id_token") {
@@ -391,23 +384,25 @@ class LoginPage extends React.Component {
}; };
if (res.status === "ok") { if (res.status === "ok") {
callback(res); if (res.data === NextMfa) {
} else if (res.status === NextMfa) { this.setState({
this.setState({ getVerifyTotp: () => {
getVerifyTotp: () => { return (
return ( <MfaAuthVerifyForm
<MfaAuthVerifyForm mfaProps={res.data2}
mfaProps={res.data} formValues={values}
formValues={values} oAuthParams={oAuthParams}
oAuthParams={oAuthParams} application={this.getApplicationObj()}
application={this.getApplicationObj()} onFail={() => {
onFail={() => { Setting.showMessage("error", i18next.t("mfa:Verification failed"));
Setting.showMessage("error", i18next.t("mfa:Verification failed")); }}
}} onSuccess={(res) => callback(res)}
onSuccess={(res) => callback(res)} />);
/>); },
}, });
}); } else {
callback(res);
}
} else { } else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
} }
@@ -998,4 +993,4 @@ class LoginPage extends React.Component {
} }
} }
export default LoginPage; export default withRouter(LoginPage);

View File

@@ -12,181 +12,55 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React, {useState} from "react"; import React from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd"; import {Button, Col, Result, Row, Steps} from "antd";
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";
import i18next from "i18next"; import i18next from "i18next";
import * as MfaBackend from "../backend/MfaBackend"; import * as MfaBackend from "../backend/MfaBackend";
import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons"; import {CheckOutlined, KeyOutlined, UserOutlined} from "@ant-design/icons";
import CheckPasswordForm from "./mfa/CheckPasswordForm";
import * as UserBackend from "../backend/UserBackend"; import MfaEnableForm from "./mfa/MfaEnableForm";
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm"; import {MfaVerifyForm} from "./mfa/MfaVerifyForm";
export const EmailMfaType = "email"; export const EmailMfaType = "email";
export const SmsMfaType = "sms"; export const SmsMfaType = "sms";
export const TotpMfaType = "app"; export const TotpMfaType = "app";
export const RecoveryMfaType = "recovery"; export const RecoveryMfaType = "recovery";
function CheckPasswordForm({user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({password}) => {
const data = {...user, password};
UserBackend.checkUserPassword(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.finally(() => {
form.setFieldsValue({password: ""});
});
};
return (
<Form
form={form}
style={{width: "300px", marginTop: "20px"}}
onFinish={onFinish}
>
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
}
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
})
.finally(() => {
form.setFieldsValue({passcode: ""});
});
};
if (mfaProps === undefined || mfaProps === null) {
return <div></div>;
}
if (mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType) {
return <MfaSmsVerifyForm mfaProps={mfaProps} onFinish={onFinish} application={application} method={mfaSetup} user={user} />;
} else if (mfaProps.mfaType === TotpMfaType) {
return <MfaTotpVerifyForm mfaProps={mfaProps} onFinish={onFinish} />;
} else {
return <div></div>;
}
}
function EnableMfaForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableTotp = () => {
const data = {
mfaType,
...user,
};
setLoading(true);
MfaBackend.MfaSetupEnable(data).then(res => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
}
).finally(() => {
setLoading(false);
});
};
return (
<div style={{width: "400px"}}>
<p>{i18next.t("mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code")}</p>
<br />
<code style={{fontStyle: "solid"}}>{recoveryCodes[0]}</code>
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableTotp();
}} block type="primary">
{i18next.t("general:Enable")}
</Button>
</div>
);
}
class MfaSetupPage extends React.Component { class MfaSetupPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const params = new URLSearchParams(props.location.search);
const {location} = this.props;
this.state = { this.state = {
account: props.account, account: props.account,
application: this.props.application ?? null, application: null,
applicationName: props.account.signupApplication ?? "", applicationName: props.account.signupApplication ?? "",
isAuthenticated: props.isAuthenticated ?? false, current: location.state?.from !== undefined ? 1 : 0,
isPromptPage: props.isPromptPage,
redirectUri: props.redirectUri,
current: props.current ?? 0,
mfaType: props.mfaType ?? new URLSearchParams(props.location?.search)?.get("mfaType") ?? SmsMfaType,
mfaProps: null, mfaProps: null,
mfaType: params.get("mfaType") ?? SmsMfaType,
isPromptPage: props.isPromptPage || location.state?.from !== undefined,
}; };
} }
componentDidMount() { componentDidMount() {
this.getApplication(); this.getApplication();
if (this.state.current === 1) {
this.initMfaProps();
}
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.isAuthenticated === true && (this.state.mfaProps === null || this.state.mfaType !== prevState.mfaType)) { if (this.state.mfaType !== prevState.mfaType || this.state.current !== prevState.current) {
MfaBackend.MfaSetupInitiate({ if (this.state.current === 1) {
mfaType: this.state.mfaType, this.initMfaProps();
...this.getUser(), }
}).then((res) => {
if (res.status === "ok") {
this.setState({
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}
});
} }
} }
getApplication() { getApplication() {
if (this.state.application !== null) {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName) ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((res) => { .then((res) => {
if (res !== null) { if (res !== null) {
@@ -203,11 +77,75 @@ class MfaSetupPage extends React.Component {
}); });
} }
initMfaProps() {
MfaBackend.MfaSetupInitiate({
mfaType: this.state.mfaType,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
this.setState({
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}
});
}
getUser() { getUser() {
return { return this.props.account;
name: this.state.account.name, }
owner: this.state.account.owner,
renderMfaTypeSwitch() {
const renderSmsLink = () => {
if (this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: SmsMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${SmsMfaType}`);
}
}>{i18next.t("mfa:Use SMS")}</Button>
);
}; };
const renderEmailLink = () => {
if (this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: EmailMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${EmailMfaType}`);
}
}>{i18next.t("mfa:Use Email")}</Button>
);
};
const renderTotpLink = () => {
if (this.state.mfaType === TotpMfaType || this.props.account.totpSecret !== "") {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: TotpMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${TotpMfaType}`);
}
}>{i18next.t("mfa:Use Authenticator App")}</Button>
);
};
return !this.state.isPromptPage ? (
<React.Fragment>
{renderSmsLink()}
{renderEmailLink()}
{renderTotpLink()}
</React.Fragment>
) : null;
} }
renderStep() { renderStep() {
@@ -219,19 +157,14 @@ class MfaSetupPage extends React.Component {
onSuccess={() => { onSuccess={() => {
this.setState({ this.setState({
current: this.state.current + 1, current: this.state.current + 1,
isAuthenticated: true,
}); });
}} }}
onFail={(res) => { onFail={(res) => {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA")); Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA") + ": " + res.msg);
}} }}
/> />
); );
case 1: case 1:
if (!this.state.isAuthenticated) {
return null;
}
return ( return (
<div> <div>
<MfaVerifyForm <MfaVerifyForm
@@ -244,52 +177,25 @@ class MfaSetupPage extends React.Component {
}); });
}} }}
onFail={(res) => { onFail={(res) => {
Setting.showMessage("error", i18next.t("general:Failed to verify")); Setting.showMessage("error", i18next.t("general:Failed to verify") + ": " + res.msg);
}} }}
/> />
<Col span={24} style={{display: "flex", justifyContent: "left"}}> <Col span={24} style={{display: "flex", justifyContent: "left"}}>
{(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null : {this.renderMfaTypeSwitch()}
<Button type={"link"} onClick={() => {
this.setState({
mfaType: EmailMfaType,
});
}
}>{i18next.t("mfa:Use Email")}</Button>
}
{
(this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: SmsMfaType,
});
}
}>{i18next.t("mfa:Use SMS")}</Button>
}
{
(this.state.mfaType === TotpMfaType) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: TotpMfaType,
});
}
}>{i18next.t("mfa:Use Authenticator App")}</Button>
}
</Col> </Col>
</div> </div>
); );
case 2: case 2:
if (!this.state.isAuthenticated) {
return null;
}
return ( return (
<EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes} <MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
onSuccess={() => { onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully")); Setting.showMessage("success", i18next.t("general:Enabled successfully"));
if (this.state.isPromptPage && this.state.redirectUri) { this.props.onfinish();
Setting.goToLink(this.state.redirectUri); if (localStorage.getItem("mfaRedirectUrl") !== null) {
Setting.goToLink(localStorage.getItem("mfaRedirectUrl"));
localStorage.removeItem("mfaRedirectUrl");
} else { } else {
Setting.goToLink("/account"); this.props.history.push("/account");
} }
}} }}
onFail={(res) => { onFail={(res) => {
@@ -308,7 +214,7 @@ class MfaSetupPage extends React.Component {
status="403" status="403"
title="403 Unauthorized" title="403 Unauthorized"
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")} subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} extra={<a href="/web/public"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
/> />
); );
} }
@@ -343,4 +249,4 @@ class MfaSetupPage extends React.Component {
} }
} }
export default MfaSetupPage; export default withRouter(MfaSetupPage);

View File

@@ -1,193 +0,0 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Form, Input, QRCode, Space} from "antd";
import i18next from "i18next";
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {SendCodeInput} from "../common/SendCodeInput";
import * as Setting from "../Setting";
import React, {useEffect} from "react";
import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
export const mfaAuth = "mfaAuth";
export const mfaSetup = "mfaSetup";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method, user}) => {
const [dest, setDest] = React.useState(mfaProps.secret ?? "");
const [form] = Form.useForm();
useEffect(() => {
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
}
}, [mfaProps.mfaType]);
const isEmail = () => {
return mfaProps.mfaType === EmailMfaType;
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
initialValues={{
countryCode: mfaProps.countryCode,
}}
>
{dest !== "" ?
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
(<React.Fragment>
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
)
}
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<SendCodeInput
countryCode={form.getFieldValue("countryCode")}
method={method}
onButtonClickArgs={[mfaProps.secret || dest, isEmail() ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};
export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm();
const renderSecret = () => {
if (!mfaProps.secret) {
return null;
}
return (
<React.Fragment>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<QRCode
errorLevel="H"
value={mfaProps.url}
icon={"https://cdn.casdoor.com/static/favicon.png"}
/>
</Col>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Scan the QR code with your Authenticator App")}</p>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Or copy the secret to your Authenticator App")}</p>
<Col span={24}>
<Space>
<Input value={mfaProps.secret} />
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Space>
</Col>
</React.Fragment>
);
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
{renderSecret()}
<Form.Item
name="passcode"
rules={[{required: true, message: "Please input your passcode"}]}
>
<Input
style={{marginTop: 24}}
prefix={<UserOutlined />}
placeholder={i18next.t("mfa:Passcode")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};

View File

@@ -16,14 +16,13 @@ import React from "react";
import {Button, Card, Col, Result, Row} from "antd"; import {Button, Card, Col, Result, Row} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import * as AuthBackend from "./AuthBackend";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import AffiliationSelect from "../common/select/AffiliationSelect"; import AffiliationSelect from "../common/select/AffiliationSelect";
import OAuthWidget from "../common/OAuthWidget"; import OAuthWidget from "../common/OAuthWidget";
import RegionSelect from "../common/select/RegionSelect"; import RegionSelect from "../common/select/RegionSelect";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import MfaSetupPage from "./MfaSetupPage"; import * as AuthBackend from "./AuthBackend";
class PromptPage extends React.Component { class PromptPage extends React.Component {
constructor(props) { constructor(props) {
@@ -34,7 +33,9 @@ class PromptPage extends React.Component {
applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName), applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName),
application: null, application: null,
user: null, user: null,
promptType: new URLSearchParams(this.props.location.search).get("promptType"), steps: null,
current: 0,
finished: false,
}; };
} }
@@ -45,6 +46,12 @@ class PromptPage extends React.Component {
} }
} }
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.user !== null && this.getApplicationObj() !== null && this.state.steps === null) {
this.initSteps(this.state.user, this.getApplicationObj());
}
}
getUser() { getUser() {
const organizationName = this.props.account.owner; const organizationName = this.props.account.owner;
const userName = this.props.account.name; const userName = this.props.account.name;
@@ -198,22 +205,25 @@ class PromptPage extends React.Component {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.onUpdateAccount(null); this.onUpdateAccount(null);
let redirectUrl = this.getRedirectUrl();
if (redirectUrl === "") {
redirectUrl = res.data2;
}
if (redirectUrl !== "" && redirectUrl !== null) {
Setting.goToLink(redirectUrl);
} else {
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
}
} else { } else {
Setting.showMessage("error", `Failed to log out: ${res.msg}`); Setting.showMessage("error", res.msg);
} }
}); });
} }
finishAndJump() {
this.setState({
finished: true,
}, () => {
const redirectUrl = this.getRedirectUrl();
if (redirectUrl !== "" && redirectUrl !== null) {
Setting.goToLink(redirectUrl);
} else {
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
}
});
}
submitUserEdit(isFinal) { submitUserEdit(isFinal) {
const user = Setting.deepCopy(this.state.user); const user = Setting.deepCopy(this.state.user);
UserBackend.updateUser(this.state.user.owner, this.state.user.name, user) UserBackend.updateUser(this.state.user.owner, this.state.user.name, user)
@@ -221,8 +231,7 @@ class PromptPage extends React.Component {
if (res.status === "ok") { if (res.status === "ok") {
if (isFinal) { if (isFinal) {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.finishAndJump();
this.logout();
} }
} else { } else {
if (isFinal) { if (isFinal) {
@@ -238,25 +247,45 @@ class PromptPage extends React.Component {
} }
renderPromptProvider(application) { renderPromptProvider(application) {
return <> return (
{this.renderContent(application)} <div style={{display: "flex", alignItems: "center", flexDirection: "column"}}>
<div style={{marginTop: "50px"}}> {this.renderContent(application)}
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button> <Button style={{marginTop: "50px", width: "200px"}}
</div> disabled={!Setting.isPromptAnswered(this.state.user, application)}
</>; type="primary" size="large" onClick={() => {
this.submitUserEdit(true);
}}>
{i18next.t("code:Submit and complete")}
</Button>
</div>);
} }
renderPromptMfa() { initSteps(user, application) {
const steps = [];
if (!Setting.isPromptAnswered(user, application) && this.state.promptType === "provider") {
steps.push({
content: this.renderPromptProvider(application),
name: "provider",
title: i18next.t("application:Binding providers"),
});
}
this.setState({
steps: steps,
});
}
renderSteps() {
if (this.state.steps === null || this.state.steps?.length === 0) {
return null;
}
return ( return (
<MfaSetupPage <Card style={{marginTop: "20px", marginBottom: "20px"}}
application={this.getApplicationObj()} title={this.state.steps[this.state.current].title}
account={this.props.account} >
current={1} <div >{this.state.steps[this.state.current].content}</div>
isAuthenticated={true} </Card>
isPromptPage={true}
redirectUri={this.getRedirectUrl()}
{...this.props}
/>
); );
} }
@@ -266,7 +295,7 @@ class PromptPage extends React.Component {
return null; return null;
} }
if (!Setting.hasPromptPage(application) && this.state.promptType !== "mfa") { if (this.state.steps?.length === 0) {
return ( return (
<Result <Result
style={{display: "flex", flex: "1 1 0%", justifyContent: "center", flexDirection: "column"}} style={{display: "flex", flex: "1 1 0%", justifyContent: "center", flexDirection: "column"}}
@@ -287,17 +316,7 @@ class PromptPage extends React.Component {
return ( return (
<div style={{display: "flex", flex: "1", justifyContent: "center"}}> <div style={{display: "flex", flex: "1", justifyContent: "center"}}>
<Card> {this.renderSteps()}
<div style={{marginTop: "30px", marginBottom: "30px", textAlign: "center"}}>
{
Setting.renderHelmet(application)
}
{
Setting.renderLogo(application)
}
{this.state.promptType !== "mfa" ? this.renderPromptProvider(application) : this.renderPromptMfa(application)}
</div>
</Card>
</div> </div>
); );
} }

View File

@@ -448,7 +448,7 @@ export function getAuthUrl(application, provider, method) {
} else if (provider.type === "Douyin" || provider.type === "TikTok") { } else if (provider.type === "Douyin" || provider.type === "TikTok") {
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Custom") { } else if (provider.type === "Custom") {
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`; return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
} else if (provider.type === "Bilibili") { } else if (provider.type === "Bilibili") {
return `${endpoint}#/?client_id=${provider.clientId}&return_url=${redirectUri}&state=${state}&response_type=code`; return `${endpoint}#/?client_id=${provider.clientId}&return_url=${redirectUri}&state=${state}&response_type=code`;
} else if (provider.type === "Deezer") { } else if (provider.type === "Deezer") {

View File

@@ -0,0 +1,56 @@
import {LockOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd";
import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
function CheckPasswordForm({user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({password}) => {
const data = {...user, password};
UserBackend.checkUserPassword(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.finally(() => {
form.setFieldsValue({password: ""});
});
};
return (
<Form
form={form}
style={{width: "300px", marginTop: "20px"}}
onFinish={onFinish}
>
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
}
export default CheckPasswordForm;

View File

@@ -15,9 +15,11 @@
import React, {useState} from "react"; import React, {useState} from "react";
import i18next from "i18next"; import i18next from "i18next";
import {Button, Input} from "antd"; import {Button, Input} from "antd";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "../AuthBackend";
import {EmailMfaType, RecoveryMfaType, SmsMfaType} from "./MfaSetupPage"; import {EmailMfaType, RecoveryMfaType, SmsMfaType} from "../MfaSetupPage";
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaAuth} from "./MfaVerifyForm"; import {mfaAuth} from "./MfaVerifyForm";
import MfaVerifySmsForm from "./MfaVerifySmsForm";
import MfaVerifyTotpForm from "./MfaVerifyTotpForm";
export const NextMfa = "NextMfa"; export const NextMfa = "NextMfa";
export const RequiredMfa = "RequiredMfa"; export const RequiredMfa = "RequiredMfa";
@@ -70,13 +72,13 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
{i18next.t("mfa:Multi-factor authentication description")} {i18next.t("mfa:Multi-factor authentication description")}
</div> </div>
{mfaType === SmsMfaType || mfaType === EmailMfaType ? ( {mfaType === SmsMfaType || mfaType === EmailMfaType ? (
<MfaSmsVerifyForm <MfaVerifySmsForm
mfaProps={mfaProps} mfaProps={mfaProps}
method={mfaAuth} method={mfaAuth}
onFinish={verify} onFinish={verify}
application={application} application={application}
/>) : ( />) : (
<MfaTotpVerifyForm <MfaVerifyTotpForm
mfaProps={mfaProps} mfaProps={mfaProps}
onFinish={verify} onFinish={verify}
/> />

View File

@@ -0,0 +1,40 @@
import {Button} from "antd";
import i18next from "i18next";
import React, {useState} from "react";
import * as MfaBackend from "../../backend/MfaBackend";
export function MfaEnableForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableMfa = () => {
const data = {
mfaType,
...user,
};
setLoading(true);
MfaBackend.MfaSetupEnable(data).then(res => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
}
).finally(() => {
setLoading(false);
});
};
return (
<div style={{width: "400px"}}>
<p>{i18next.t("mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code")}</p>
<br />
<code style={{fontStyle: "solid"}}>{recoveryCodes[0]}</code>
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableMfa();
}} block type="primary">
{i18next.t("general:Enable")}
</Button>
</div>
);
}
export default MfaEnableForm;

View File

@@ -0,0 +1,58 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Form} from "antd";
import i18next from "i18next";
import * as MfaBackend from "../../backend/MfaBackend";
import * as Setting from "../../Setting";
import React from "react";
import {EmailMfaType, SmsMfaType, TotpMfaType} from "../MfaSetupPage";
import MfaVerifySmsForm from "./MfaVerifySmsForm";
import MfaVerifyTotpForm from "./MfaVerifyTotpForm";
export const mfaAuth = "mfaAuth";
export const mfaSetup = "mfaSetup";
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
})
.finally(() => {
form.setFieldsValue({passcode: ""});
});
};
if (mfaProps === undefined || mfaProps === null || application === undefined || application === null || user === undefined || user === null) {
return <div></div>;
}
if (mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType) {
return <MfaVerifySmsForm mfaProps={mfaProps} onFinish={onFinish} application={application} method={mfaSetup} user={user} />;
} else if (mfaProps.mfaType === TotpMfaType) {
return <MfaVerifyTotpForm mfaProps={mfaProps} onFinish={onFinish} />;
} else {
return <div></div>;
}
}

View File

@@ -0,0 +1,125 @@
import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
import {SendCodeInput} from "../../common/SendCodeInput";
import * as Setting from "../../Setting";
import {EmailMfaType, SmsMfaType} from "../MfaSetupPage";
import {mfaAuth} from "./MfaVerifyForm";
export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}) => {
const [dest, setDest] = React.useState("");
const [form] = Form.useForm();
useEffect(() => {
if (method === mfaAuth) {
setDest(mfaProps.secret);
return;
}
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
return;
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
}
}, [mfaProps.mfaType]);
const isShowText = () => {
if (method === mfaAuth) {
return true;
}
if (mfaProps.mfaType === SmsMfaType && user.phone !== "") {
return true;
}
if (mfaProps.mfaType === EmailMfaType && user.email !== "") {
return true;
}
return false;
};
const isEmail = () => {
return mfaProps.mfaType === EmailMfaType;
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
initialValues={{
countryCode: mfaProps.countryCode,
}}
>
{isShowText() ?
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
(<React.Fragment>
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
)
}
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<SendCodeInput
countryCode={form.getFieldValue("countryCode")}
method={method}
onButtonClickArgs={[mfaProps.secret || dest, isEmail() ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};
export default MfaVerifySmsForm;

View File

@@ -0,0 +1,79 @@
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {Button, Col, Form, Input, QRCode, Space} from "antd";
import copy from "copy-to-clipboard";
import i18next from "i18next";
import React from "react";
import * as Setting from "../../Setting";
export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm();
const renderSecret = () => {
if (!mfaProps.secret) {
return null;
}
return (
<React.Fragment>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<QRCode
errorLevel="H"
value={mfaProps.url}
icon={"https://cdn.casdoor.com/static/favicon.png"}
/>
</Col>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Scan the QR code with your Authenticator App")}</p>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Or copy the secret to your Authenticator App")}</p>
<Col span={24}>
<Space>
<Input value={mfaProps.secret} />
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Space>
</Col>
</React.Fragment>
);
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
{renderSecret()}
<Form.Item
name="passcode"
rules={[{required: true, message: "Please input your passcode"}]}
>
<Input
style={{marginTop: 24}}
prefix={<UserOutlined />}
placeholder={i18next.t("mfa:Passcode")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};
export default MfaVerifyTotpForm;

View File

@@ -28,6 +28,9 @@ export const CropperDivModal = (props) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const {title} = props; const {title} = props;
const {setTitle} = props;
const {tag} = props;
const {disabled} = props;
const {user} = props; const {user} = props;
const {buttonText} = props; const {buttonText} = props;
const {organization} = props; const {organization} = props;
@@ -59,8 +62,8 @@ export const CropperDivModal = (props) => {
} }
// Setting.showMessage("success", "uploading..."); // Setting.showMessage("success", "uploading...");
const extension = image.substring(image.indexOf("/") + 1, image.indexOf(";base64")); const extension = image.substring(image.indexOf("/") + 1, image.indexOf(";base64"));
const fullFilePath = `avatar/${user.owner}/${user.name}.${extension}`; const fullFilePath = `${tag}/${user.owner}/${user.name}.${extension}`;
ResourceBackend.uploadResource(user.owner, user.name, "avatar", "CropperDivModal", fullFilePath, blob) ResourceBackend.uploadResource(user.owner, user.name, tag, "CropperDivModal", fullFilePath, blob)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
window.location.href = window.location.pathname; window.location.href = window.location.pathname;
@@ -139,19 +142,19 @@ export const CropperDivModal = (props) => {
return ( return (
<div> <div>
<Button type="default" onClick={showModal}> <Button type="default" onClick={showModal} disabled={disabled}>
{buttonText} {buttonText}
</Button> </Button>
<Modal <Modal
maskClosable={false} maskClosable={false}
title={title} title={title}
open={visible} open={visible}
okText={i18next.t("user:Upload a photo")} okText={title}
confirmLoading={confirmLoading} confirmLoading={confirmLoading}
onCancel={handleCancel} onCancel={handleCancel}
width={600} width={600}
footer={ footer={
[<Button block key="submit" type="primary" onClick={handleOk}>{i18next.t("user:Set new profile picture")}</Button>] [<Button block key="submit" type="primary" onClick={handleOk}>{setTitle}</Button>]
} }
> >
<Col style={{margin: "0px auto 60px auto", width: 1000, height: 350}}> <Col style={{margin: "0px auto 60px auto", width: 1000, height: 350}}>

View File

@@ -0,0 +1,87 @@
import {Button, Space, Tag, notification} from "antd";
import i18next from "i18next";
import {useEffect} from "react";
import {useHistory, useLocation} from "react-router-dom";
import * as Setting from "../../Setting";
import {MfaRulePrompted, MfaRuleRequired} from "../../Setting";
const EnableMfaNotification = ({account}) => {
const [api, contextHolder] = notification.useNotification();
const history = useHistory();
const location = useLocation();
useEffect(() => {
if (account === null) {
return;
}
const mfaItems = Setting.getMfaItemsByRules(account, account?.organization, [MfaRuleRequired, MfaRulePrompted]);
if (location.state?.from === "/login" && mfaItems.length !== 0) {
if (mfaItems.some((item) => item.rule === MfaRuleRequired)) {
openRequiredEnableNotification(mfaItems.find((item) => item.rule === MfaRuleRequired).name);
} else {
openPromptEnableNotification(mfaItems.filter((item) => item.rule === MfaRulePrompted)?.map((item) => item.name));
}
}
}, [account, location.state?.from]);
const openPromptEnableNotification = (mfaTypes) => {
const key = `open${Date.now()}`;
const btn = (
<Space>
<Button type="link" size="small" onClick={() => api.destroy(key)}>
{i18next.t("general:Later")}
</Button>
<Button type="primary" size="small" onClick={() => {
history.push(`/mfa/setup?mfaType=${mfaTypes[0]}`, {from: "/"});
api.destroy(key);
}}
>
{i18next.t("general:Go to enable")}
</Button>
</Space>
);
api.open({
message: i18next.t("mfa:Enable multi-factor authentication"),
description:
<Space direction={"vertical"}>
{i18next.t("mfa:To ensure the security of your account, it is recommended that you enable multi-factor authentication")}
<Space>{mfaTypes.map((item) => <Tag color="orange" key={item}>{item}</Tag>)}</Space>
</Space>,
btn,
key,
});
};
const openRequiredEnableNotification = (mfaType) => {
const key = `open${Date.now()}`;
const btn = (
<Space>
<Button type="primary" size="small" onClick={() => {
api.destroy(key);
}}
>
{i18next.t("general:Confirm")}
</Button>
</Space>
);
api.open({
message: i18next.t("mfa:Enable multi-factor authentication"),
description:
<Space direction={"vertical"}>
{i18next.t("mfa:To ensure the security of your account, it is required to enable multi-factor authentication")}
<Space><Tag color="orange">{mfaType}</Tag></Space>
</Space>,
btn,
key,
});
};
return (
<>
{contextHolder}
</>
);
};
export default EnableMfaNotification;

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Wenn eine angemeldete Session in Casdoor vorhanden ist, wird diese automatisch für die Anmeldung auf Anwendungsebene verwendet", "Auto signin - Tooltip": "Wenn eine angemeldete Session in Casdoor vorhanden ist, wird diese automatisch für die Anmeldung auf Anwendungsebene verwendet",
"Background URL": "Background-URL", "Background URL": "Background-URL",
"Background URL - Tooltip": "URL des Hintergrundbildes, das auf der Anmeldeseite angezeigt wird", "Background URL - Tooltip": "URL des Hintergrundbildes, das auf der Anmeldeseite angezeigt wird",
"Binding providers": "Binding providers",
"Center": "Zentrum", "Center": "Zentrum",
"Copy SAML metadata URL": "SAML-Metadaten-URL kopieren", "Copy SAML metadata URL": "SAML-Metadaten-URL kopieren",
"Copy prompt page URL": "URL der Prompt-Seite kopieren", "Copy prompt page URL": "URL der Prompt-Seite kopieren",
@@ -227,6 +228,7 @@
"Forget URL": "Passwort vergessen URL", "Forget URL": "Passwort vergessen URL",
"Forget URL - Tooltip": "Benutzerdefinierte URL für die \"Passwort vergessen\" Seite. Wenn nicht festgelegt, wird die standardmäßige Casdoor \"Passwort vergessen\" Seite verwendet. Wenn sie festgelegt ist, wird der \"Passwort vergessen\" Link auf der Login-Seite zu dieser URL umgeleitet", "Forget URL - Tooltip": "Benutzerdefinierte URL für die \"Passwort vergessen\" Seite. Wenn nicht festgelegt, wird die standardmäßige Casdoor \"Passwort vergessen\" Seite verwendet. Wenn sie festgelegt ist, wird der \"Passwort vergessen\" Link auf der Login-Seite zu dieser URL umgeleitet",
"Found some texts still not translated? Please help us translate at": "Haben Sie noch Texte gefunden, die nicht übersetzt wurden? Bitte helfen Sie uns beim Übersetzen", "Found some texts still not translated? Please help us translate at": "Haben Sie noch Texte gefunden, die nicht übersetzt wurden? Bitte helfen Sie uns beim Übersetzen",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?", "Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Sprachen", "Languages": "Sprachen",
"Languages - Tooltip": "Verfügbare Sprachen", "Languages - Tooltip": "Verfügbare Sprachen",
"Last name": "Nachname", "Last name": "Nachname",
"Later": "Later",
"Logo": "Logo", "Logo": "Logo",
"Logo - Tooltip": "Symbole, die die Anwendung der Außenwelt präsentiert", "Logo - Tooltip": "Symbole, die die Anwendung der Außenwelt präsentiert",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "Regel ändern", "Modify rule": "Regel ändern",
"New Organization": "Neue Organisation", "New Organization": "Neue Organisation",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Softe Löschung", "Soft deletion": "Softe Löschung",
"Soft deletion - Tooltip": "Wenn aktiviert, werden gelöschte Benutzer nicht vollständig aus der Datenbank entfernt. Stattdessen werden sie als gelöscht markiert", "Soft deletion - Tooltip": "Wenn aktiviert, werden gelöschte Benutzer nicht vollständig aus der Datenbank entfernt. Stattdessen werden sie als gelöscht markiert",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Preisseite URL kopieren", "Copy pricing page URL": "Preisseite URL kopieren",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Kostenlos",
"Failed to get plans": "Es konnten keine Pläne abgerufen werden", "Failed to get plans": "Es konnten keine Pläne abgerufen werden",
"Free": "Kostenlos",
"Getting started": "Loslegen", "Getting started": "Loslegen",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Testphase Dauer", "Trial duration": "Testphase Dauer",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "Token-URL", "Token URL - Tooltip": "Token-URL",
"Type": "Typ", "Type": "Typ",
"Type - Tooltip": "Wählen Sie einen Typ aus", "Type - Tooltip": "Wählen Sie einen Typ aus",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "UserInfo-URL", "UserInfo URL": "UserInfo-URL",
"UserInfo URL - Tooltip": "UserInfo-URL", "UserInfo URL - Tooltip": "UserInfo-URL",
"admin (Shared)": "admin (Shared)" "admin (Shared)": "admin (Shared)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "Homepage-URL des Benutzers", "Homepage - Tooltip": "Homepage-URL des Benutzers",
"ID card": "Ausweis", "ID card": "Ausweis",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Geben Sie Ihre E-Mail-Adresse ein", "Input your email": "Geben Sie Ihre E-Mail-Adresse ein",
"Input your phone number": "Geben Sie Ihre Telefonnummer ein", "Input your phone number": "Geben Sie Ihre Telefonnummer ein",
"Is admin": "Ist Admin", "Is admin": "Ist Admin",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "Zwei von Ihnen eingegebene Passwörter stimmen nicht überein.", "Two passwords you typed do not match.": "Zwei von Ihnen eingegebene Passwörter stimmen nicht überein.",
"Unlink": "Link aufheben", "Unlink": "Link aufheben",
"Upload (.xlsx)": "Hochladen (.xlsx)", "Upload (.xlsx)": "Hochladen (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Lade ein Foto hoch", "Upload a photo": "Lade ein Foto hoch",
"Values": "Werte", "Values": "Werte",
"Verification code sent": "Bestätigungscode gesendet", "Verification code sent": "Bestätigungscode gesendet",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "When a logged-in session exists in Casdoor, it is automatically used for application-side login", "Auto signin - Tooltip": "When a logged-in session exists in Casdoor, it is automatically used for application-side login",
"Background URL": "Background URL", "Background URL": "Background URL",
"Background URL - Tooltip": "URL of the background image used in the login page", "Background URL - Tooltip": "URL of the background image used in the login page",
"Binding providers": "Binding providers",
"Center": "Center", "Center": "Center",
"Copy SAML metadata URL": "Copy SAML metadata URL", "Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL", "Copy prompt page URL": "Copy prompt page URL",
@@ -227,6 +228,7 @@
"Forget URL": "Forget URL", "Forget URL": "Forget URL",
"Forget URL - Tooltip": "Custom URL for the \"Forget password\" page. If not set, the default Casdoor \"Forget password\" page will be used. When set, the \"Forget password\" link on the login page will redirect to this URL", "Forget URL - Tooltip": "Custom URL for the \"Forget password\" page. If not set, the default Casdoor \"Forget password\" page will be used. When set, the \"Forget password\" link on the login page will redirect to this URL",
"Found some texts still not translated? Please help us translate at": "Found some texts still not translated? Please help us translate at", "Found some texts still not translated? Please help us translate at": "Found some texts still not translated? Please help us translate at",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Go to writable demo site?", "Go to writable demo site?": "Go to writable demo site?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Languages", "Languages": "Languages",
"Languages - Tooltip": "Available languages", "Languages - Tooltip": "Available languages",
"Last name": "Last name", "Last name": "Last name",
"Later": "Later",
"Logo": "Logo", "Logo": "Logo",
"Logo - Tooltip": "Icons that the application presents to the outside world", "Logo - Tooltip": "Icons that the application presents to the outside world",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,15 +429,16 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ", "Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Setup Multi-factor authentication", "Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "If you are unable to access your device, enter your recovery code to verify your identity", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App", "Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "Modify rule", "Modify rule": "Modify rule",
"New Organization": "New Organization", "New Organization": "New Organization",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Soft deletion", "Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "When enabled, deleting users will not completely remove them from the database. Instead, they will be marked as deleted", "Soft deletion - Tooltip": "When enabled, deleting users will not completely remove them from the database. Instead, they will be marked as deleted",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Copy pricing page URL", "Copy pricing page URL": "Copy pricing page URL",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Free",
"Failed to get plans": "Failed to get plans", "Failed to get plans": "Failed to get plans",
"Free": "Free",
"Getting started": "Getting started", "Getting started": "Getting started",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Trial duration", "Trial duration": "Trial duration",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "Token URL", "Token URL - Tooltip": "Token URL",
"Type": "Type", "Type": "Type",
"Type - Tooltip": "Select a type", "Type - Tooltip": "Select a type",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "UserInfo URL", "UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL", "UserInfo URL - Tooltip": "UserInfo URL",
"admin (Shared)": "admin (Shared)" "admin (Shared)": "admin (Shared)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "Homepage URL of the user", "Homepage - Tooltip": "Homepage URL of the user",
"ID card": "ID card", "ID card": "ID card",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Input your email", "Input your email": "Input your email",
"Input your phone number": "Input your phone number", "Input your phone number": "Input your phone number",
"Is admin": "Is admin", "Is admin": "Is admin",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "Two passwords you typed do not match.", "Two passwords you typed do not match.": "Two passwords you typed do not match.",
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)", "Upload (.xlsx)": "Upload (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"Values": "Values", "Values": "Values",
"Verification code sent": "Verification code sent", "Verification code sent": "Verification code sent",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Cuando existe una sesión iniciada en Casdoor, se utiliza automáticamente para el inicio de sesión del lado de la aplicación", "Auto signin - Tooltip": "Cuando existe una sesión iniciada en Casdoor, se utiliza automáticamente para el inicio de sesión del lado de la aplicación",
"Background URL": "URL de fondo", "Background URL": "URL de fondo",
"Background URL - Tooltip": "URL de la imagen de fondo utilizada en la página de inicio de sesión", "Background URL - Tooltip": "URL de la imagen de fondo utilizada en la página de inicio de sesión",
"Binding providers": "Binding providers",
"Center": "Centro", "Center": "Centro",
"Copy SAML metadata URL": "Copia la URL de metadatos SAML", "Copy SAML metadata URL": "Copia la URL de metadatos SAML",
"Copy prompt page URL": "Copiar URL de la página del prompt", "Copy prompt page URL": "Copiar URL de la página del prompt",
@@ -227,6 +228,7 @@
"Forget URL": "Olvide la URL", "Forget URL": "Olvide la URL",
"Forget URL - Tooltip": "URL personalizada para la página \"Olvidé mi contraseña\". Si no se establece, se utilizará la página \"Olvidé mi contraseña\" predeterminada de Casdoor. Cuando se establezca, el enlace \"Olvidé mi contraseña\" en la página de inicio de sesión redireccionará a esta URL", "Forget URL - Tooltip": "URL personalizada para la página \"Olvidé mi contraseña\". Si no se establece, se utilizará la página \"Olvidé mi contraseña\" predeterminada de Casdoor. Cuando se establezca, el enlace \"Olvidé mi contraseña\" en la página de inicio de sesión redireccionará a esta URL",
"Found some texts still not translated? Please help us translate at": "¿Encontraste algunos textos que aún no están traducidos? Por favor, ayúdanos a traducirlos en", "Found some texts still not translated? Please help us translate at": "¿Encontraste algunos textos que aún no están traducidos? Por favor, ayúdanos a traducirlos en",
"Go to enable": "Go to enable",
"Go to writable demo site?": "¿Ir al sitio demo editable?", "Go to writable demo site?": "¿Ir al sitio demo editable?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Idiomas", "Languages": "Idiomas",
"Languages - Tooltip": "Idiomas disponibles", "Languages - Tooltip": "Idiomas disponibles",
"Last name": "Apellido", "Last name": "Apellido",
"Later": "Later",
"Logo": "Logotipo", "Logo": "Logotipo",
"Logo - Tooltip": "Iconos que la aplicación presenta al mundo exterior", "Logo - Tooltip": "Iconos que la aplicación presenta al mundo exterior",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "Modificar regla", "Modify rule": "Modificar regla",
"New Organization": "Nueva organización", "New Organization": "Nueva organización",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Eliminación suave", "Soft deletion": "Eliminación suave",
"Soft deletion - Tooltip": "Cuando se habilita, la eliminación de usuarios no los eliminará por completo de la base de datos. En su lugar, se marcarán como eliminados", "Soft deletion - Tooltip": "Cuando se habilita, la eliminación de usuarios no los eliminará por completo de la base de datos. En su lugar, se marcarán como eliminados",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Copiar URL de la página de precios", "Copy pricing page URL": "Copiar URL de la página de precios",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Gratis",
"Failed to get plans": "No se pudieron obtener los planes", "Failed to get plans": "No se pudieron obtener los planes",
"Free": "Gratis",
"Getting started": "Empezar", "Getting started": "Empezar",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Duración del período de prueba", "Trial duration": "Duración del período de prueba",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "URL de token", "Token URL - Tooltip": "URL de token",
"Type": "Tipo", "Type": "Tipo",
"Type - Tooltip": "Seleccionar un tipo", "Type - Tooltip": "Seleccionar un tipo",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "URL de información del usuario", "UserInfo URL": "URL de información del usuario",
"UserInfo URL - Tooltip": "URL de información de usuario", "UserInfo URL - Tooltip": "URL de información de usuario",
"admin (Shared)": "administrador (compartido)" "admin (Shared)": "administrador (compartido)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "URL de la página de inicio del usuario", "Homepage - Tooltip": "URL de la página de inicio del usuario",
"ID card": "Tarjeta de identificación", "ID card": "Tarjeta de identificación",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Introduce tu correo electrónico", "Input your email": "Introduce tu correo electrónico",
"Input your phone number": "Ingrese su número de teléfono", "Input your phone number": "Ingrese su número de teléfono",
"Is admin": "Es el administrador", "Is admin": "Es el administrador",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "Dos contraseñas que has escrito no coinciden.", "Two passwords you typed do not match.": "Dos contraseñas que has escrito no coinciden.",
"Unlink": "Desvincular", "Unlink": "Desvincular",
"Upload (.xlsx)": "Subir (.xlsx)", "Upload (.xlsx)": "Subir (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Subir una foto", "Upload a photo": "Subir una foto",
"Values": "Valores", "Values": "Valores",
"Verification code sent": "Código de verificación enviado", "Verification code sent": "Código de verificación enviado",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Lorsqu'une session connectée existe dans Casdoor, elle est automatiquement utilisée pour la connexion côté application", "Auto signin - Tooltip": "Lorsqu'une session connectée existe dans Casdoor, elle est automatiquement utilisée pour la connexion côté application",
"Background URL": "URL de fond", "Background URL": "URL de fond",
"Background URL - Tooltip": "\"L'URL de l'image de fond utilisée sur la page de connexion\"", "Background URL - Tooltip": "\"L'URL de l'image de fond utilisée sur la page de connexion\"",
"Binding providers": "Binding providers",
"Center": "Centre", "Center": "Centre",
"Copy SAML metadata URL": "Copiez l'URL de métadonnées SAML", "Copy SAML metadata URL": "Copiez l'URL de métadonnées SAML",
"Copy prompt page URL": "Copier l'URL de la page de l'invite", "Copy prompt page URL": "Copier l'URL de la page de l'invite",
@@ -227,6 +228,7 @@
"Forget URL": "Oubliez l'URL", "Forget URL": "Oubliez l'URL",
"Forget URL - Tooltip": "URL personnalisée pour la page \"Mot de passe oublié\". Si elle n'est pas définie, la page par défaut \"Mot de passe oublié\" de Casdoor sera utilisée. Lorsqu'elle est définie, le lien \"Mot de passe oublié\" sur la page de connexion sera redirigé vers cette URL", "Forget URL - Tooltip": "URL personnalisée pour la page \"Mot de passe oublié\". Si elle n'est pas définie, la page par défaut \"Mot de passe oublié\" de Casdoor sera utilisée. Lorsqu'elle est définie, le lien \"Mot de passe oublié\" sur la page de connexion sera redirigé vers cette URL",
"Found some texts still not translated? Please help us translate at": "Trouvé des textes encore non traduits ? Veuillez nous aider à les traduire", "Found some texts still not translated? Please help us translate at": "Trouvé des textes encore non traduits ? Veuillez nous aider à les traduire",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Allez sur le site de démonstration modifiable ?", "Go to writable demo site?": "Allez sur le site de démonstration modifiable ?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Langues", "Languages": "Langues",
"Languages - Tooltip": "Langues disponibles", "Languages - Tooltip": "Langues disponibles",
"Last name": "Nom de famille", "Last name": "Nom de famille",
"Later": "Later",
"Logo": "Logo", "Logo": "Logo",
"Logo - Tooltip": "Icônes que l'application présente au monde extérieur", "Logo - Tooltip": "Icônes que l'application présente au monde extérieur",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "Modifier la règle", "Modify rule": "Modifier la règle",
"New Organization": "Nouvelle organisation", "New Organization": "Nouvelle organisation",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Suppression douce", "Soft deletion": "Suppression douce",
"Soft deletion - Tooltip": "Lorsqu'elle est activée, la suppression d'utilisateurs ne les retirera pas complètement de la base de données. Au lieu de cela, ils seront marqués comme supprimés", "Soft deletion - Tooltip": "Lorsqu'elle est activée, la suppression d'utilisateurs ne les retirera pas complètement de la base de données. Au lieu de cela, ils seront marqués comme supprimés",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Copier l'URL de la page tarifs", "Copy pricing page URL": "Copier l'URL de la page tarifs",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Gratuit",
"Failed to get plans": "Échec de l'obtention des plans", "Failed to get plans": "Échec de l'obtention des plans",
"Free": "Gratuit",
"Getting started": "Commencer", "Getting started": "Commencer",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Durée de l'essai", "Trial duration": "Durée de l'essai",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "URL de jeton", "Token URL - Tooltip": "URL de jeton",
"Type": "Type", "Type": "Type",
"Type - Tooltip": "Sélectionnez un type", "Type - Tooltip": "Sélectionnez un type",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "URL d'informations utilisateur", "UserInfo URL": "URL d'informations utilisateur",
"UserInfo URL - Tooltip": "URL d'informations sur l'utilisateur", "UserInfo URL - Tooltip": "URL d'informations sur l'utilisateur",
"admin (Shared)": "admin (Partagé)" "admin (Shared)": "admin (Partagé)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "Adresse URL de la page d'accueil de l'utilisateur", "Homepage - Tooltip": "Adresse URL de la page d'accueil de l'utilisateur",
"ID card": "carte d'identité", "ID card": "carte d'identité",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Entrez votre adresse e-mail", "Input your email": "Entrez votre adresse e-mail",
"Input your phone number": "Saisissez votre numéro de téléphone", "Input your phone number": "Saisissez votre numéro de téléphone",
"Is admin": "Est l'administrateur", "Is admin": "Est l'administrateur",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "Deux mots de passe que vous avez tapés ne correspondent pas.", "Two passwords you typed do not match.": "Deux mots de passe que vous avez tapés ne correspondent pas.",
"Unlink": "Détacher", "Unlink": "Détacher",
"Upload (.xlsx)": "Télécharger (.xlsx)", "Upload (.xlsx)": "Télécharger (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Télécharger une photo", "Upload a photo": "Télécharger une photo",
"Values": "Valeurs", "Values": "Valeurs",
"Verification code sent": "Code de vérification envoyé", "Verification code sent": "Code de vérification envoyé",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Ketika sesi masuk yang terdaftar ada di Casdoor, secara otomatis digunakan untuk masuk ke sisi aplikasi", "Auto signin - Tooltip": "Ketika sesi masuk yang terdaftar ada di Casdoor, secara otomatis digunakan untuk masuk ke sisi aplikasi",
"Background URL": "URL latar belakang", "Background URL": "URL latar belakang",
"Background URL - Tooltip": "URL dari gambar latar belakang yang digunakan di halaman login", "Background URL - Tooltip": "URL dari gambar latar belakang yang digunakan di halaman login",
"Binding providers": "Binding providers",
"Center": "pusat", "Center": "pusat",
"Copy SAML metadata URL": "Salin URL metadata SAML", "Copy SAML metadata URL": "Salin URL metadata SAML",
"Copy prompt page URL": "Salin URL halaman prompt", "Copy prompt page URL": "Salin URL halaman prompt",
@@ -227,6 +228,7 @@
"Forget URL": "Lupakan URL", "Forget URL": "Lupakan URL",
"Forget URL - Tooltip": "URL kustom untuk halaman \"Lupa kata sandi\". Jika tidak diatur, halaman \"Lupa kata sandi\" default Casdoor akan digunakan. Ketika diatur, tautan \"Lupa kata sandi\" pada halaman masuk akan diarahkan ke URL ini", "Forget URL - Tooltip": "URL kustom untuk halaman \"Lupa kata sandi\". Jika tidak diatur, halaman \"Lupa kata sandi\" default Casdoor akan digunakan. Ketika diatur, tautan \"Lupa kata sandi\" pada halaman masuk akan diarahkan ke URL ini",
"Found some texts still not translated? Please help us translate at": "Menemukan beberapa teks yang masih belum diterjemahkan? Tolong bantu kami menerjemahkan di", "Found some texts still not translated? Please help us translate at": "Menemukan beberapa teks yang masih belum diterjemahkan? Tolong bantu kami menerjemahkan di",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Pergi ke situs demo yang dapat ditulis?", "Go to writable demo site?": "Pergi ke situs demo yang dapat ditulis?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Bahasa-bahasa", "Languages": "Bahasa-bahasa",
"Languages - Tooltip": "Bahasa yang tersedia", "Languages - Tooltip": "Bahasa yang tersedia",
"Last name": "Nama belakang", "Last name": "Nama belakang",
"Later": "Later",
"Logo": "Logo", "Logo": "Logo",
"Logo - Tooltip": "Ikon-ikon yang disajikan aplikasi ke dunia luar", "Logo - Tooltip": "Ikon-ikon yang disajikan aplikasi ke dunia luar",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "Mengubah aturan", "Modify rule": "Mengubah aturan",
"New Organization": "Organisasi baru", "New Organization": "Organisasi baru",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Penghapusan lunak", "Soft deletion": "Penghapusan lunak",
"Soft deletion - Tooltip": "Ketika diaktifkan, menghapus pengguna tidak akan sepenuhnya menghapus mereka dari database. Sebaliknya, mereka akan ditandai sebagai dihapus", "Soft deletion - Tooltip": "Ketika diaktifkan, menghapus pengguna tidak akan sepenuhnya menghapus mereka dari database. Sebaliknya, mereka akan ditandai sebagai dihapus",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Salin URL halaman harga", "Copy pricing page URL": "Salin URL halaman harga",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Gratis",
"Failed to get plans": "Gagal mendapatkan rencana", "Failed to get plans": "Gagal mendapatkan rencana",
"Free": "Gratis",
"Getting started": "Mulai", "Getting started": "Mulai",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Durasi percobaan", "Trial duration": "Durasi percobaan",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "Token URL: URL Token", "Token URL - Tooltip": "Token URL: URL Token",
"Type": "Jenis", "Type": "Jenis",
"Type - Tooltip": "Pilih tipe", "Type - Tooltip": "Pilih tipe",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "URL UserInfo", "UserInfo URL": "URL UserInfo",
"UserInfo URL - Tooltip": "URL Informasi Pengguna", "UserInfo URL - Tooltip": "URL Informasi Pengguna",
"admin (Shared)": "Admin (Berbagi)" "admin (Shared)": "Admin (Berbagi)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "URL halaman depan pengguna", "Homepage - Tooltip": "URL halaman depan pengguna",
"ID card": "Kartu identitas", "ID card": "Kartu identitas",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Masukkan alamat email Anda", "Input your email": "Masukkan alamat email Anda",
"Input your phone number": "Masukkan nomor telepon Anda", "Input your phone number": "Masukkan nomor telepon Anda",
"Is admin": "Apakah admin?", "Is admin": "Apakah admin?",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "Dua password yang Anda ketikkan tidak cocok.", "Two passwords you typed do not match.": "Dua password yang Anda ketikkan tidak cocok.",
"Unlink": "Membatalkan Tautan", "Unlink": "Membatalkan Tautan",
"Upload (.xlsx)": "Unggah (.xlsx)", "Upload (.xlsx)": "Unggah (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Unggah foto", "Upload a photo": "Unggah foto",
"Values": "Nilai-nilai", "Values": "Nilai-nilai",
"Verification code sent": "Kode verifikasi telah dikirim", "Verification code sent": "Kode verifikasi telah dikirim",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Casdoorにログインセッションが存在する場合、アプリケーション側のログインに自動的に使用されます", "Auto signin - Tooltip": "Casdoorにログインセッションが存在する場合、アプリケーション側のログインに自動的に使用されます",
"Background URL": "背景URL", "Background URL": "背景URL",
"Background URL - Tooltip": "ログインページで使用される背景画像のURL", "Background URL - Tooltip": "ログインページで使用される背景画像のURL",
"Binding providers": "Binding providers",
"Center": "センター", "Center": "センター",
"Copy SAML metadata URL": "SAMLメタデータのURLをコピーしてください", "Copy SAML metadata URL": "SAMLメタデータのURLをコピーしてください",
"Copy prompt page URL": "プロンプトページのURLをコピーしてください", "Copy prompt page URL": "プロンプトページのURLをコピーしてください",
@@ -96,7 +97,7 @@
"Signup items": "サインアップアイテム", "Signup items": "サインアップアイテム",
"Signup items - Tooltip": "新しいアカウントを登録する際にユーザーが入力するアイテム", "Signup items - Tooltip": "新しいアカウントを登録する際にユーザーが入力するアイテム",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "サインアップページのURLがクリップボードに正常にコピーされました。シークレットウィンドウまたは別のブラウザに貼り付けてください", "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "サインアップページのURLがクリップボードに正常にコピーされました。シークレットウィンドウまたは別のブラウザに貼り付けてください",
"Tags - Tooltip": "Only users with the tag that is listed in the application tags can login", "Tags - Tooltip": "Only users with the tag that is listed in the application tags can login",
"The application does not allow to sign up new account": "アプリケーションでは新しいアカウントの登録ができません", "The application does not allow to sign up new account": "アプリケーションでは新しいアカウントの登録ができません",
"Token expire": "トークンの有効期限が切れました", "Token expire": "トークンの有効期限が切れました",
"Token expire - Tooltip": "アクセストークンの有効期限", "Token expire - Tooltip": "アクセストークンの有効期限",
@@ -227,6 +228,7 @@
"Forget URL": "URLを忘れてください", "Forget URL": "URLを忘れてください",
"Forget URL - Tooltip": "「パスワードをお忘れの場合」ページのカスタムURL。未設定の場合、デフォルトのCasdoor「パスワードをお忘れの場合」ページが使用されます。設定された場合、ログインページの「パスワードをお忘れの場合」リンクはこのURLにリダイレクトされます", "Forget URL - Tooltip": "「パスワードをお忘れの場合」ページのカスタムURL。未設定の場合、デフォルトのCasdoor「パスワードをお忘れの場合」ページが使用されます。設定された場合、ログインページの「パスワードをお忘れの場合」リンクはこのURLにリダイレクトされます",
"Found some texts still not translated? Please help us translate at": "まだ翻訳されていない文章が見つかりましたか?是非とも翻訳のお手伝いをお願いします", "Found some texts still not translated? Please help us translate at": "まだ翻訳されていない文章が見つかりましたか?是非とも翻訳のお手伝いをお願いします",
"Go to enable": "Go to enable",
"Go to writable demo site?": "書き込み可能なデモサイトに移動しますか?", "Go to writable demo site?": "書き込み可能なデモサイトに移動しますか?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "言語", "Languages": "言語",
"Languages - Tooltip": "利用可能な言語", "Languages - Tooltip": "利用可能な言語",
"Last name": "苗字", "Last name": "苗字",
"Later": "Later",
"Logo": "ロゴ", "Logo": "ロゴ",
"Logo - Tooltip": "アプリケーションが外部世界に示すアイコン", "Logo - Tooltip": "アプリケーションが外部世界に示すアイコン",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "ルールを変更する", "Modify rule": "ルールを変更する",
"New Organization": "新しい組織", "New Organization": "新しい組織",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "ソフト削除", "Soft deletion": "ソフト削除",
"Soft deletion - Tooltip": "有効になっている場合、ユーザーを削除しても完全にデータベースから削除されません。代わりに、削除されたとマークされます", "Soft deletion - Tooltip": "有効になっている場合、ユーザーを削除しても完全にデータベースから削除されません。代わりに、削除されたとマークされます",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "価格ページのURLをコピー", "Copy pricing page URL": "価格ページのURLをコピー",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "無料",
"Failed to get plans": "計画の取得に失敗しました", "Failed to get plans": "計画の取得に失敗しました",
"Free": "無料",
"Getting started": "はじめる", "Getting started": "はじめる",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "トライアル期間の長さ", "Trial duration": "トライアル期間の長さ",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "トークンURL", "Token URL - Tooltip": "トークンURL",
"Type": "タイプ", "Type": "タイプ",
"Type - Tooltip": "タイプを選択してください", "Type - Tooltip": "タイプを選択してください",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "UserInfo URLを日本語に翻訳すると、「ユーザー情報のURL」となります", "UserInfo URL": "UserInfo URLを日本語に翻訳すると、「ユーザー情報のURL」となります",
"UserInfo URL - Tooltip": "ユーザー情報URL", "UserInfo URL - Tooltip": "ユーザー情報URL",
"admin (Shared)": "管理者(共有)" "admin (Shared)": "管理者(共有)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "ユーザーのホームページのURL", "Homepage - Tooltip": "ユーザーのホームページのURL",
"ID card": "IDカード", "ID card": "IDカード",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "あなたのメールアドレスを入力してください", "Input your email": "あなたのメールアドレスを入力してください",
"Input your phone number": "電話番号を入力してください", "Input your phone number": "電話番号を入力してください",
"Is admin": "管理者ですか?", "Is admin": "管理者ですか?",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "2つのパスワードが一致しません。", "Two passwords you typed do not match.": "2つのパスワードが一致しません。",
"Unlink": "アンリンク", "Unlink": "アンリンク",
"Upload (.xlsx)": "アップロード(.xlsx", "Upload (.xlsx)": "アップロード(.xlsx",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "写真をアップロードしてください", "Upload a photo": "写真をアップロードしてください",
"Values": "価値観", "Values": "価値観",
"Verification code sent": "確認コードを送信しました", "Verification code sent": "確認コードを送信しました",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "카스도어에 로그인된 세션이 존재할 때, 애플리케이션 쪽 로그인에 자동으로 사용됩니다", "Auto signin - Tooltip": "카스도어에 로그인된 세션이 존재할 때, 애플리케이션 쪽 로그인에 자동으로 사용됩니다",
"Background URL": "배경 URL", "Background URL": "배경 URL",
"Background URL - Tooltip": "로그인 페이지에서 사용된 배경 이미지의 URL", "Background URL - Tooltip": "로그인 페이지에서 사용된 배경 이미지의 URL",
"Binding providers": "Binding providers",
"Center": "중앙", "Center": "중앙",
"Copy SAML metadata URL": "SAML 메타데이터 URL 복사", "Copy SAML metadata URL": "SAML 메타데이터 URL 복사",
"Copy prompt page URL": "프롬프트 페이지 URL을 복사하세요", "Copy prompt page URL": "프롬프트 페이지 URL을 복사하세요",
@@ -227,6 +228,7 @@
"Forget URL": "URL을 잊어버려라", "Forget URL": "URL을 잊어버려라",
"Forget URL - Tooltip": "\"비밀번호를 잊어버렸을 경우\" 페이지에 대한 사용자 정의 URL. 설정되지 않은 경우 기본 Casdoor \"비밀번호를 잊어버렸을 경우\" 페이지가 사용됩니다. 설정된 경우 로그인 페이지의 \"비밀번호를 잊으셨나요?\" 링크는 이 URL로 리디렉션됩니다", "Forget URL - Tooltip": "\"비밀번호를 잊어버렸을 경우\" 페이지에 대한 사용자 정의 URL. 설정되지 않은 경우 기본 Casdoor \"비밀번호를 잊어버렸을 경우\" 페이지가 사용됩니다. 설정된 경우 로그인 페이지의 \"비밀번호를 잊으셨나요?\" 링크는 이 URL로 리디렉션됩니다",
"Found some texts still not translated? Please help us translate at": "아직 번역되지 않은 텍스트가 있나요? 번역에 도움을 주실 수 있나요?", "Found some texts still not translated? Please help us translate at": "아직 번역되지 않은 텍스트가 있나요? 번역에 도움을 주실 수 있나요?",
"Go to enable": "Go to enable",
"Go to writable demo site?": "쓰기 가능한 데모 사이트로 이동하시겠습니까?", "Go to writable demo site?": "쓰기 가능한 데모 사이트로 이동하시겠습니까?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "언어", "Languages": "언어",
"Languages - Tooltip": "사용 가능한 언어", "Languages - Tooltip": "사용 가능한 언어",
"Last name": "성", "Last name": "성",
"Later": "Later",
"Logo": "로고", "Logo": "로고",
"Logo - Tooltip": "애플리케이션이 외부 세계에 제시하는 아이콘들", "Logo - Tooltip": "애플리케이션이 외부 세계에 제시하는 아이콘들",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "규칙 수정", "Modify rule": "규칙 수정",
"New Organization": "새로운 조직", "New Organization": "새로운 조직",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "소프트 삭제", "Soft deletion": "소프트 삭제",
"Soft deletion - Tooltip": "사용 가능한 경우, 사용자 삭제 시 데이터베이스에서 완전히 삭제되지 않습니다. 대신 삭제됨으로 표시됩니다", "Soft deletion - Tooltip": "사용 가능한 경우, 사용자 삭제 시 데이터베이스에서 완전히 삭제되지 않습니다. 대신 삭제됨으로 표시됩니다",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "가격 페이지 URL 복사", "Copy pricing page URL": "가격 페이지 URL 복사",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "무료",
"Failed to get plans": "계획을 가져오지 못했습니다.", "Failed to get plans": "계획을 가져오지 못했습니다.",
"Free": "무료",
"Getting started": "시작하기", "Getting started": "시작하기",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "체험 기간", "Trial duration": "체험 기간",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "토큰 URL", "Token URL - Tooltip": "토큰 URL",
"Type": "타입", "Type": "타입",
"Type - Tooltip": "유형을 선택하세요", "Type - Tooltip": "유형을 선택하세요",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "사용자 정보 URL", "UserInfo URL": "사용자 정보 URL",
"UserInfo URL - Tooltip": "UserInfo URL: 사용자 정보 URL", "UserInfo URL - Tooltip": "UserInfo URL: 사용자 정보 URL",
"admin (Shared)": "관리자 (공유)" "admin (Shared)": "관리자 (공유)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "사용자의 홈페이지 URL", "Homepage - Tooltip": "사용자의 홈페이지 URL",
"ID card": "ID 카드", "ID card": "ID 카드",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "이메일을 입력하세요", "Input your email": "이메일을 입력하세요",
"Input your phone number": "전화번호를 입력하세요", "Input your phone number": "전화번호를 입력하세요",
"Is admin": "어드민인가요?", "Is admin": "어드민인가요?",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "두 개의 비밀번호가 일치하지 않습니다.", "Two passwords you typed do not match.": "두 개의 비밀번호가 일치하지 않습니다.",
"Unlink": "연결 해제하기", "Unlink": "연결 해제하기",
"Upload (.xlsx)": "업로드 (.xlsx)", "Upload (.xlsx)": "업로드 (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "사진을 업로드하세요", "Upload a photo": "사진을 업로드하세요",
"Values": "가치들", "Values": "가치들",
"Verification code sent": "인증 코드가 전송되었습니다", "Verification code sent": "인증 코드가 전송되었습니다",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Quando uma sessão logada existe no Casdoor, ela é automaticamente usada para o login no lado da aplicação", "Auto signin - Tooltip": "Quando uma sessão logada existe no Casdoor, ela é automaticamente usada para o login no lado da aplicação",
"Background URL": "URL de Fundo", "Background URL": "URL de Fundo",
"Background URL - Tooltip": "URL da imagem de fundo usada na página de login", "Background URL - Tooltip": "URL da imagem de fundo usada na página de login",
"Binding providers": "Binding providers",
"Center": "Centro", "Center": "Centro",
"Copy SAML metadata URL": "Copiar URL de metadados SAML", "Copy SAML metadata URL": "Copiar URL de metadados SAML",
"Copy prompt page URL": "Copiar URL da página de prompt", "Copy prompt page URL": "Copiar URL da página de prompt",
@@ -227,6 +228,7 @@
"Forget URL": "URL de Esqueci a Senha", "Forget URL": "URL de Esqueci a Senha",
"Forget URL - Tooltip": "URL personalizada para a página de \"Esqueci a senha\". Se não definido, será usada a página padrão de \"Esqueci a senha\" do Casdoor. Quando definido, o link de \"Esqueci a senha\" na página de login será redirecionado para esta URL", "Forget URL - Tooltip": "URL personalizada para a página de \"Esqueci a senha\". Se não definido, será usada a página padrão de \"Esqueci a senha\" do Casdoor. Quando definido, o link de \"Esqueci a senha\" na página de login será redirecionado para esta URL",
"Found some texts still not translated? Please help us translate at": "Encontrou algum texto ainda não traduzido? Ajude-nos a traduzir em", "Found some texts still not translated? Please help us translate at": "Encontrou algum texto ainda não traduzido? Ajude-nos a traduzir em",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Acessar o site de demonstração gravável?", "Go to writable demo site?": "Acessar o site de demonstração gravável?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Idiomas", "Languages": "Idiomas",
"Languages - Tooltip": "Idiomas disponíveis", "Languages - Tooltip": "Idiomas disponíveis",
"Last name": "Sobrenome", "Last name": "Sobrenome",
"Later": "Later",
"Logo": "Logo", "Logo": "Logo",
"Logo - Tooltip": "Ícones que o aplicativo apresenta para o mundo externo", "Logo - Tooltip": "Ícones que o aplicativo apresenta para o mundo externo",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -425,38 +428,41 @@
"Text - Tooltip": "Texto - Dica de ferramenta" "Text - Tooltip": "Texto - Dica de ferramenta"
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Cada vez que você entrar na sua Conta, você precisará da sua senha e de um código de autenticação", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Falha ao obter o aplicativo", "Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to initiate MFA": "Falha ao iniciar MFA", "Failed to get application": "Failed to get application",
"Have problems?": "Está com problemas?", "Failed to initiate MFA": "Failed to initiate MFA",
"Multi-factor authentication": "Autenticação de vários fatores", "Have problems?": "Have problems?",
"Multi-factor authentication - Tooltip ": "Autenticação de vários fatores - Dica de ferramenta", "Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication description": "Configurar autenticação de vários fatores", "Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor methods": "Métodos de vários fatores", "Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor recover": "Recuperação de vários fatores", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover description": "Se você não conseguir acessar seu dispositivo, insira seu código de recuperação para verificar sua identidade", "Multi-factor recover": "Multi-factor recover",
"Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App", "Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Código de acesso", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Código de recuperação", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Definir preferido", "Set preferred": "Set preferred",
"Setup": "Configuração", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Usar código de verificação SMS", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Usar um código de recuperação", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verificação falhou", "Verification failed": "Verification failed",
"Verify Code": "Verificar Código", "Verify Code": "Verify Code",
"Verify Password": "Verificar Senha", "Verify Password": "Verify Password",
"Your email is": "Seu e-mail é", "Your email is": "Your email is",
"Your phone is": "Seu telefone é", "Your phone is": "Your phone is",
"preferred": "Preferido" "preferred": "preferred"
}, },
"model": { "model": {
"Edit Model": "Editar Modelo", "Edit Model": "Editar Modelo",
@@ -477,6 +483,7 @@
"Modify rule": "Modificar regra", "Modify rule": "Modificar regra",
"New Organization": "Nova Organização", "New Organization": "Nova Organização",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Exclusão suave", "Soft deletion": "Exclusão suave",
"Soft deletion - Tooltip": "Quando ativada, a exclusão de usuários não os removerá completamente do banco de dados. Em vez disso, eles serão marcados como excluídos", "Soft deletion - Tooltip": "Quando ativada, a exclusão de usuários não os removerá completamente do banco de dados. Em vez disso, eles serão marcados como excluídos",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Sao chép URL trang bảng giá", "Copy pricing page URL": "Sao chép URL trang bảng giá",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Miễn phí",
"Failed to get plans": "Falha ao obter planos", "Failed to get plans": "Falha ao obter planos",
"Free": "Miễn phí",
"Getting started": "Bắt đầu", "Getting started": "Bắt đầu",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Thời gian thử nghiệm", "Trial duration": "Thời gian thử nghiệm",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "URL do Token", "Token URL - Tooltip": "URL do Token",
"Type": "Tipo", "Type": "Tipo",
"Type - Tooltip": "Selecione um tipo", "Type - Tooltip": "Selecione um tipo",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "URL do UserInfo", "UserInfo URL": "URL do UserInfo",
"UserInfo URL - Tooltip": "URL do UserInfo", "UserInfo URL - Tooltip": "URL do UserInfo",
"admin (Shared)": "admin (Compartilhado)" "admin (Shared)": "admin (Compartilhado)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "URL da página inicial do usuário", "Homepage - Tooltip": "URL da página inicial do usuário",
"ID card": "Cartão de identidade", "ID card": "Cartão de identidade",
"ID card - Tooltip": "Cartão de identidade - Tooltip", "ID card - Tooltip": "Cartão de identidade - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "Tipo de cartão de identidade", "ID card type": "Tipo de cartão de identidade",
"ID card type - Tooltip": "Tipo de cartão de identidade - Tooltip", "ID card type - Tooltip": "Tipo de cartão de identidade - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Digite seu e-mail", "Input your email": "Digite seu e-mail",
"Input your phone number": "Digite seu número de telefone", "Input your phone number": "Digite seu número de telefone",
"Is admin": "É administrador", "Is admin": "É administrador",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "As duas senhas digitadas não coincidem.", "Two passwords you typed do not match.": "As duas senhas digitadas não coincidem.",
"Unlink": "Desvincular", "Unlink": "Desvincular",
"Upload (.xlsx)": "Enviar (.xlsx)", "Upload (.xlsx)": "Enviar (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Enviar uma foto", "Upload a photo": "Enviar uma foto",
"Values": "Valores", "Values": "Valores",
"Verification code sent": "Código de verificação enviado", "Verification code sent": "Código de verificação enviado",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Когда существует активная сессия входа в Casdoor, она автоматически используется для входа на стороне приложения", "Auto signin - Tooltip": "Когда существует активная сессия входа в Casdoor, она автоматически используется для входа на стороне приложения",
"Background URL": "Фоновый URL", "Background URL": "Фоновый URL",
"Background URL - Tooltip": "URL фонового изображения, используемого на странице входа", "Background URL - Tooltip": "URL фонового изображения, используемого на странице входа",
"Binding providers": "Binding providers",
"Center": "Центр", "Center": "Центр",
"Copy SAML metadata URL": "Скопируйте URL метаданных SAML", "Copy SAML metadata URL": "Скопируйте URL метаданных SAML",
"Copy prompt page URL": "Скопируйте URL страницы предложения", "Copy prompt page URL": "Скопируйте URL страницы предложения",
@@ -227,6 +228,7 @@
"Forget URL": "Забудьте URL", "Forget URL": "Забудьте URL",
"Forget URL - Tooltip": "Настроенный URL для страницы \"Забыли пароль\". Если не установлено, будет использоваться стандартная страница \"Забыли пароль\" Casdoor. При установке, ссылка \"Забыли пароль\" на странице входа будет перенаправляться на этот URL", "Forget URL - Tooltip": "Настроенный URL для страницы \"Забыли пароль\". Если не установлено, будет использоваться стандартная страница \"Забыли пароль\" Casdoor. При установке, ссылка \"Забыли пароль\" на странице входа будет перенаправляться на этот URL",
"Found some texts still not translated? Please help us translate at": "Нашли некоторые тексты, которые еще не переведены? Пожалуйста, помогите нам перевести на", "Found some texts still not translated? Please help us translate at": "Нашли некоторые тексты, которые еще не переведены? Пожалуйста, помогите нам перевести на",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Перейти на демонстрационный сайт для записи данных?", "Go to writable demo site?": "Перейти на демонстрационный сайт для записи данных?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Языки", "Languages": "Языки",
"Languages - Tooltip": "Доступные языки", "Languages - Tooltip": "Доступные языки",
"Last name": "Фамилия", "Last name": "Фамилия",
"Later": "Later",
"Logo": "Логотип", "Logo": "Логотип",
"Logo - Tooltip": "Иконки, которые приложение представляет во внешний мир", "Logo - Tooltip": "Иконки, которые приложение представляет во внешний мир",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "Изменить правило", "Modify rule": "Изменить правило",
"New Organization": "Новая организация", "New Organization": "Новая организация",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Мягкое удаление", "Soft deletion": "Мягкое удаление",
"Soft deletion - Tooltip": "Когда включено, удаление пользователей не полностью удаляет их из базы данных. Вместо этого они будут помечены как удаленные", "Soft deletion - Tooltip": "Когда включено, удаление пользователей не полностью удаляет их из базы данных. Вместо этого они будут помечены как удаленные",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Скопировать URL прайс-листа", "Copy pricing page URL": "Скопировать URL прайс-листа",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Бесплатно",
"Failed to get plans": "Не удалось получить планы", "Failed to get plans": "Не удалось получить планы",
"Free": "Бесплатно",
"Getting started": "Выьрать план", "Getting started": "Выьрать план",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Продолжительность пробного периода", "Trial duration": "Продолжительность пробного периода",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "Токен URL", "Token URL - Tooltip": "Токен URL",
"Type": "Тип", "Type": "Тип",
"Type - Tooltip": "Выберите тип", "Type - Tooltip": "Выберите тип",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "URL информации о пользователе", "UserInfo URL": "URL информации о пользователе",
"UserInfo URL - Tooltip": "URL пользовательской информации (URL информации о пользователе)", "UserInfo URL - Tooltip": "URL пользовательской информации (URL информации о пользователе)",
"admin (Shared)": "администратор (общий)" "admin (Shared)": "администратор (общий)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "URL домашней страницы пользователя", "Homepage - Tooltip": "URL домашней страницы пользователя",
"ID card": "ID-карта", "ID card": "ID-карта",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Введите свой адрес электронной почты", "Input your email": "Введите свой адрес электронной почты",
"Input your phone number": "Введите ваш номер телефона", "Input your phone number": "Введите ваш номер телефона",
"Is admin": "Это администратор", "Is admin": "Это администратор",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "Два введенных вами пароля не совпадают.", "Two passwords you typed do not match.": "Два введенных вами пароля не совпадают.",
"Unlink": "Отсоединить", "Unlink": "Отсоединить",
"Upload (.xlsx)": "Загрузить (.xlsx)", "Upload (.xlsx)": "Загрузить (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Загрузить фото", "Upload a photo": "Загрузить фото",
"Values": "Значения", "Values": "Значения",
"Verification code sent": "Код подтверждения отправлен", "Verification code sent": "Код подтверждения отправлен",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Khi một phiên đăng nhập đã được tạo trong Casdoor, nó sẽ tự động được sử dụng để đăng nhập tại ứng dụng", "Auto signin - Tooltip": "Khi một phiên đăng nhập đã được tạo trong Casdoor, nó sẽ tự động được sử dụng để đăng nhập tại ứng dụng",
"Background URL": "URL nền", "Background URL": "URL nền",
"Background URL - Tooltip": "Đường dẫn URL của hình ảnh nền được sử dụng trong trang đăng nhập", "Background URL - Tooltip": "Đường dẫn URL của hình ảnh nền được sử dụng trong trang đăng nhập",
"Binding providers": "Binding providers",
"Center": "Trung tâm", "Center": "Trung tâm",
"Copy SAML metadata URL": "Sao chép URL siêu dữ liệu SAML", "Copy SAML metadata URL": "Sao chép URL siêu dữ liệu SAML",
"Copy prompt page URL": "Sao chép URL của trang nhắc nhở", "Copy prompt page URL": "Sao chép URL của trang nhắc nhở",
@@ -227,6 +228,7 @@
"Forget URL": "Quên đường dẫn URL", "Forget URL": "Quên đường dẫn URL",
"Forget URL - Tooltip": "Đường dẫn tùy chỉnh cho trang \"Quên mật khẩu\". Nếu không được thiết lập, trang \"Quên mật khẩu\" mặc định của Casdoor sẽ được sử dụng. Khi cài đặt, liên kết \"Quên mật khẩu\" trên trang đăng nhập sẽ chuyển hướng đến URL này", "Forget URL - Tooltip": "Đường dẫn tùy chỉnh cho trang \"Quên mật khẩu\". Nếu không được thiết lập, trang \"Quên mật khẩu\" mặc định của Casdoor sẽ được sử dụng. Khi cài đặt, liên kết \"Quên mật khẩu\" trên trang đăng nhập sẽ chuyển hướng đến URL này",
"Found some texts still not translated? Please help us translate at": "Tìm thấy một số văn bản vẫn chưa được dịch? Vui lòng giúp chúng tôi dịch tại", "Found some texts still not translated? Please help us translate at": "Tìm thấy một số văn bản vẫn chưa được dịch? Vui lòng giúp chúng tôi dịch tại",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Bạn có muốn đi đến trang demo có thể viết được không?", "Go to writable demo site?": "Bạn có muốn đi đến trang demo có thể viết được không?",
"Groups": "Groups", "Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "Ngôn ngữ", "Languages": "Ngôn ngữ",
"Languages - Tooltip": "Các ngôn ngữ hiện có", "Languages - Tooltip": "Các ngôn ngữ hiện có",
"Last name": "Họ", "Last name": "Họ",
"Later": "Later",
"Logo": "Logo", "Logo": "Logo",
"Logo - Tooltip": "Biểu tượng mà ứng dụng hiển thị ra ngoài thế giới", "Logo - Tooltip": "Biểu tượng mà ứng dụng hiển thị ra ngoài thế giới",
"MFA items": "MFA items", "MFA items": "MFA items",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code", "Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application", "Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA", "Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?", "Have problems?": "Have problems?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App", "Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App", "Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
@@ -477,6 +483,7 @@
"Modify rule": "Sửa đổi quy tắc", "Modify rule": "Sửa đổi quy tắc",
"New Organization": "Tổ chức mới", "New Organization": "Tổ chức mới",
"Optional": "Optional", "Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required", "Required": "Required",
"Soft deletion": "Xóa mềm", "Soft deletion": "Xóa mềm",
"Soft deletion - Tooltip": "Khi được bật, việc xóa người dùng sẽ không hoàn toàn loại bỏ họ khỏi cơ sở dữ liệu. Thay vào đó, họ sẽ được đánh dấu là đã bị xóa", "Soft deletion - Tooltip": "Khi được bật, việc xóa người dùng sẽ không hoàn toàn loại bỏ họ khỏi cơ sở dữ liệu. Thay vào đó, họ sẽ được đánh dấu là đã bị xóa",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "Sao chép URL trang bảng giá", "Copy pricing page URL": "Sao chép URL trang bảng giá",
"Edit Pricing": "Edit Pricing", "Edit Pricing": "Edit Pricing",
"Free": "Miễn phí",
"Failed to get plans": "Không thể lấy được các kế hoạch", "Failed to get plans": "Không thể lấy được các kế hoạch",
"Free": "Miễn phí",
"Getting started": "Bắt đầu", "Getting started": "Bắt đầu",
"New Pricing": "New Pricing", "New Pricing": "New Pricing",
"Trial duration": "Thời gian thử nghiệm", "Trial duration": "Thời gian thử nghiệm",
@@ -739,6 +746,8 @@
"Token URL - Tooltip": "Địa chỉ URL của Token", "Token URL - Tooltip": "Địa chỉ URL của Token",
"Type": "Kiểu", "Type": "Kiểu",
"Type - Tooltip": "Chọn loại", "Type - Tooltip": "Chọn loại",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "Đường dẫn UserInfo", "UserInfo URL": "Đường dẫn UserInfo",
"UserInfo URL - Tooltip": "Địa chỉ URL của Thông tin người dùng", "UserInfo URL - Tooltip": "Địa chỉ URL của Thông tin người dùng",
"admin (Shared)": "quản trị viên (Chung)" "admin (Shared)": "quản trị viên (Chung)"
@@ -902,8 +911,13 @@
"Homepage - Tooltip": "Địa chỉ URL của trang chủ của người dùng", "Homepage - Tooltip": "Địa chỉ URL của trang chủ của người dùng",
"ID card": "Thẻ căn cước dân sự", "ID card": "Thẻ căn cước dân sự",
"ID card - Tooltip": "ID card - Tooltip", "ID card - Tooltip": "ID card - Tooltip",
"ID card back": "ID card back",
"ID card front": "ID card front",
"ID card info": "ID card info",
"ID card info - Tooltip": "ID card info - Tooltip",
"ID card type": "ID card type", "ID card type": "ID card type",
"ID card type - Tooltip": "ID card type - Tooltip", "ID card type - Tooltip": "ID card type - Tooltip",
"ID card with person": "ID card with person",
"Input your email": "Nhập địa chỉ email của bạn", "Input your email": "Nhập địa chỉ email của bạn",
"Input your phone number": "Nhập số điện thoại của bạn", "Input your phone number": "Nhập số điện thoại của bạn",
"Is admin": "Là quản trị viên", "Is admin": "Là quản trị viên",
@@ -959,6 +973,9 @@
"Two passwords you typed do not match.": "Hai mật khẩu mà bạn đã nhập không khớp.", "Two passwords you typed do not match.": "Hai mật khẩu mà bạn đã nhập không khớp.",
"Unlink": "Hủy liên kết", "Unlink": "Hủy liên kết",
"Upload (.xlsx)": "Tải lên (.xlsx)", "Upload (.xlsx)": "Tải lên (.xlsx)",
"Upload ID card back picture": "Upload ID card back picture",
"Upload ID card front picture": "Upload ID card front picture",
"Upload ID card with person picture": "Upload ID card with person picture",
"Upload a photo": "Tải lên một bức ảnh", "Upload a photo": "Tải lên một bức ảnh",
"Values": "Giá trị", "Values": "Giá trị",
"Verification code sent": "Mã xác minh đã được gửi", "Verification code sent": "Mã xác minh đã được gửi",

View File

@@ -20,6 +20,7 @@
"Auto signin - Tooltip": "当Casdoor存在已登录会话时自动采用该会话进行应用端的登录", "Auto signin - Tooltip": "当Casdoor存在已登录会话时自动采用该会话进行应用端的登录",
"Background URL": "背景图URL", "Background URL": "背景图URL",
"Background URL - Tooltip": "登录页背景图的链接", "Background URL - Tooltip": "登录页背景图的链接",
"Binding providers": "绑定提供商",
"Center": "居中", "Center": "居中",
"Copy SAML metadata URL": "复制SAML元数据URL", "Copy SAML metadata URL": "复制SAML元数据URL",
"Copy prompt page URL": "复制提醒页面URL", "Copy prompt page URL": "复制提醒页面URL",
@@ -227,6 +228,7 @@
"Forget URL": "忘记密码URL", "Forget URL": "忘记密码URL",
"Forget URL - Tooltip": "自定义忘记密码页面的URL不设置时采用Casdoor默认的忘记密码页面设置后Casdoor各类页面的忘记密码链接会跳转到该URL", "Forget URL - Tooltip": "自定义忘记密码页面的URL不设置时采用Casdoor默认的忘记密码页面设置后Casdoor各类页面的忘记密码链接会跳转到该URL",
"Found some texts still not translated? Please help us translate at": "发现有些文字尚未翻译?请移步这里帮我们翻译:", "Found some texts still not translated? Please help us translate at": "发现有些文字尚未翻译?请移步这里帮我们翻译:",
"Go to enable": "前往启用",
"Go to writable demo site?": "跳转至可写演示站点?", "Go to writable demo site?": "跳转至可写演示站点?",
"Groups": "群组", "Groups": "群组",
"Groups - Tooltip": "Groups - Tooltip", "Groups - Tooltip": "Groups - Tooltip",
@@ -241,6 +243,7 @@
"Languages": "语言", "Languages": "语言",
"Languages - Tooltip": "可选语言", "Languages - Tooltip": "可选语言",
"Last name": "姓氏", "Last name": "姓氏",
"Later": "稍后",
"Logo": "Logo", "Logo": "Logo",
"Logo - Tooltip": "应用程序向外展示的图标", "Logo - Tooltip": "应用程序向外展示的图标",
"MFA items": "MFA 项", "MFA items": "MFA 项",
@@ -426,6 +429,7 @@
}, },
"mfa": { "mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "每次登录帐户时,都需要密码和认证码", "Each time you sign in to your Account, you'll need your password and a authentication code": "每次登录帐户时,都需要密码和认证码",
"Enable multi-factor authentication": "启用多因素认证",
"Failed to get application": "获取应用失败", "Failed to get application": "获取应用失败",
"Failed to initiate MFA": "初始化 MFA 失败", "Failed to initiate MFA": "初始化 MFA 失败",
"Have problems?": "遇到问题?", "Have problems?": "遇到问题?",
@@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "用你的身份验证应用扫描二维码", "Scan the QR code with your Authenticator App": "用你的身份验证应用扫描二维码",
"Set preferred": "设为首选", "Set preferred": "设为首选",
"Setup": "设置", "Setup": "设置",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "为了确保您的帐户安全, 建议您启用多因素认证",
"To ensure the security of your account, it is required to enable multi-factor authentication": "为了确保您的帐户安全,您需要启用多因素身份验证",
"Use Authenticator App": "使用身份验证应用", "Use Authenticator App": "使用身份验证应用",
"Use Email": "使用电子邮件", "Use Email": "使用电子邮件",
"Use SMS": "使用短信", "Use SMS": "使用短信",
@@ -477,6 +483,7 @@
"Modify rule": "修改规则", "Modify rule": "修改规则",
"New Organization": "添加组织", "New Organization": "添加组织",
"Optional": "可选", "Optional": "可选",
"Prompt": "提示",
"Required": "必须", "Required": "必须",
"Soft deletion": "软删除", "Soft deletion": "软删除",
"Soft deletion - Tooltip": "启用后,删除一个用户时不会在数据库彻底清除,只会标记为已删除状态", "Soft deletion - Tooltip": "启用后,删除一个用户时不会在数据库彻底清除,只会标记为已删除状态",
@@ -570,8 +577,8 @@
"pricing": { "pricing": {
"Copy pricing page URL": "复制定价页面链接", "Copy pricing page URL": "复制定价页面链接",
"Edit Pricing": "编辑定价", "Edit Pricing": "编辑定价",
"Free": "免费",
"Failed to get plans": "未能获取计划", "Failed to get plans": "未能获取计划",
"Free": "免费",
"Getting started": "开始使用", "Getting started": "开始使用",
"New Pricing": "添加定价", "New Pricing": "添加定价",
"Trial duration": "试用期时长", "Trial duration": "试用期时长",
@@ -902,8 +909,13 @@
"Homepage - Tooltip": "个人主页链接", "Homepage - Tooltip": "个人主页链接",
"ID card": "身份证号", "ID card": "身份证号",
"ID card - Tooltip": "身份证号 - Tooltip", "ID card - Tooltip": "身份证号 - Tooltip",
"ID card back": "身份证反面",
"ID card front": "身份证正面",
"ID card info": "身份证照片",
"ID card info - Tooltip": "身份证照片用于进行用户身份验证,验证成功后如要修改请联系管理员",
"ID card type": "身份证类型", "ID card type": "身份证类型",
"ID card type - Tooltip": "身份证类型 - Tooltip", "ID card type - Tooltip": "身份证类型 - Tooltip",
"ID card with person": "手持身份证",
"Input your email": "请输入邮箱", "Input your email": "请输入邮箱",
"Input your phone number": "输入手机号", "Input your phone number": "输入手机号",
"Is admin": "是组织管理员", "Is admin": "是组织管理员",
@@ -959,6 +971,9 @@
"Two passwords you typed do not match.": "两次输入的密码不匹配。", "Two passwords you typed do not match.": "两次输入的密码不匹配。",
"Unlink": "解绑", "Unlink": "解绑",
"Upload (.xlsx)": "上传(.xlsx", "Upload (.xlsx)": "上传(.xlsx",
"Upload ID card back picture": "上传身份证反面照片",
"Upload ID card front picture": "上传身份证正面照片",
"Upload ID card with person picture": "上传手持身份证照片",
"Upload a photo": "上传头像", "Upload a photo": "上传头像",
"Values": "值", "Values": "值",
"Verification code sent": "验证码已发送", "Verification code sent": "验证码已发送",

View File

@@ -66,7 +66,7 @@ class PricingPage extends React.Component {
.then(results => { .then(results => {
const hasError = results.some(result => result.status === "error"); const hasError = results.some(result => result.status === "error");
if (hasError) { if (hasError) {
Setting.showMessage("error", `${i18next.t("Failed to get plans")}`); Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
return; return;
} }
this.setState({ this.setState({
@@ -75,7 +75,7 @@ class PricingPage extends React.Component {
}); });
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Failed to get plans: ${error}`); Setting.showMessage("error", i18next.t("pricing:Failed to get plans") + `: ${error}`);
}); });
} }

View File

@@ -80,6 +80,7 @@ class AccountTable extends React.Component {
{name: "Title", label: i18next.t("user:Title")}, {name: "Title", label: i18next.t("user:Title")},
{name: "ID card type", label: i18next.t("user:ID card type")}, {name: "ID card type", label: i18next.t("user:ID card type")},
{name: "ID card", label: i18next.t("user:ID card")}, {name: "ID card", label: i18next.t("user:ID card")},
{name: "ID card info", label: i18next.t("user:ID card info")},
{name: "Homepage", label: i18next.t("user:Homepage")}, {name: "Homepage", label: i18next.t("user:Homepage")},
{name: "Bio", label: i18next.t("user:Bio")}, {name: "Bio", label: i18next.t("user:Bio")},
{name: "Tag", label: i18next.t("user:Tag")}, {name: "Tag", label: i18next.t("user:Tag")},
@@ -92,9 +93,9 @@ class AccountTable extends React.Component {
{name: "Ranking", label: i18next.t("user:Ranking")}, {name: "Ranking", label: i18next.t("user:Ranking")},
{name: "Signup application", label: i18next.t("general:Signup application")}, {name: "Signup application", label: i18next.t("general:Signup application")},
{name: "API key", label: i18next.t("general:API key")}, {name: "API key", label: i18next.t("general:API key")},
{name: "Groups", label: i18next.t("general:Groups")},
{name: "Roles", label: i18next.t("general:Roles")}, {name: "Roles", label: i18next.t("general:Roles")},
{name: "Permissions", label: i18next.t("general:Permissions")}, {name: "Permissions", label: i18next.t("general:Permissions")},
{name: "Groups", label: i18next.t("general:Groups")},
{name: "3rd-party logins", label: i18next.t("user:3rd-party logins")}, {name: "3rd-party logins", label: i18next.t("user:3rd-party logins")},
{name: "Properties", label: i18next.t("user:Properties")}, {name: "Properties", label: i18next.t("user:Properties")},
{name: "Is online", label: i18next.t("user:Is online")}, {name: "Is online", label: i18next.t("user:Is online")},

View File

@@ -15,14 +15,23 @@
import React from "react"; import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Table, Tooltip} from "antd"; import {Button, Col, Row, Select, Table, Tooltip} from "antd";
import {EmailMfaType, SmsMfaType, TotpMfaType} from "../auth/MfaSetupPage";
import {MfaRuleOptional, MfaRulePrompted, MfaRuleRequired} from "../Setting";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
const {Option} = Select; const {Option} = Select;
const MfaItems = [ const MfaItems = [
{name: "Phone"}, {name: "Phone", value: SmsMfaType},
{name: "Email"}, {name: "Email", value: EmailMfaType},
{name: "App", value: TotpMfaType},
];
const RuleItems = [
{value: MfaRuleOptional, label: i18next.t("organization:Optional")},
{value: MfaRulePrompted, label: i18next.t("organization:Prompt")},
{value: MfaRuleRequired, label: i18next.t("organization:Required")},
]; ];
class MfaTable extends React.Component { class MfaTable extends React.Component {
@@ -80,7 +89,7 @@ class MfaTable extends React.Component {
this.updateField(table, index, "name", value); this.updateField(table, index, "name", value);
}} > }} >
{ {
Setting.getDeduplicatedArray(MfaItems, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>) Setting.getDeduplicatedArray(MfaItems, table, "name").map((item, index) => <Option key={index} value={item.value}>{item.name}</Option>)
} }
</Select> </Select>
); );
@@ -96,12 +105,21 @@ class MfaTable extends React.Component {
<Select virtual={false} style={{width: "100%"}} <Select virtual={false} style={{width: "100%"}}
value={text} value={text}
defaultValue="Optional" defaultValue="Optional"
options={[ options={RuleItems.map((item) =>
{value: "Optional", label: i18next.t("organization:Optional")},
{value: "Required", label: i18next.t("organization:Required")}].map((item) =>
Setting.getOption(item.label, item.value)) Setting.getOption(item.label, item.value))
} }
onChange={value => { onChange={value => {
let requiredCount = 0;
table.forEach((item) => {
if (item.rule === MfaRuleRequired) {
requiredCount++;
}
});
if (value === MfaRuleRequired && requiredCount >= 1) {
Setting.showMessage("error", "Only 1 MFA methods can be required");
return;
}
this.updateField(table, index, "rule", value); this.updateField(table, index, "rule", value);
}} > }} >
</Select> </Select>
@@ -135,7 +153,7 @@ class MfaTable extends React.Component {
title={() => ( title={() => (
<div> <div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp; {this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button> <Button disabled={table.length >= MfaItems.length} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div> </div>
)} )}
/> />

View File

@@ -173,7 +173,7 @@
lru-cache "^5.1.1" lru-cache "^5.1.1"
semver "^6.3.0" semver "^6.3.0"
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.22.5": "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.5":
version "7.22.5" version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz#2192a1970ece4685fbff85b48da2c32fcb130b7c" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz#2192a1970ece4685fbff85b48da2c32fcb130b7c"
integrity sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q== integrity sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==
@@ -433,6 +433,16 @@
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
"@babel/plugin-proposal-private-property-in-object@^7.21.11":
version "7.21.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c"
integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
"@babel/helper-create-class-features-plugin" "^7.21.0"
"@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-proposal-unicode-property-regex@^7.4.4": "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e"