Compare commits

...

11 Commits

Author SHA1 Message Date
June
8080b10b3b feat: show code signin page with password disabled (#2021) 2023-06-28 00:38:48 +08:00
Trần Thanh Tịnh
cd7589775c feat: replace all panic by response err (#1993)
* fix: missing return after response error

* feat: handle error in frontend

* feat: disable loading and catch org edit error

* chore: i18 for error message

* chore: remove break line

* feat: application catching error
2023-06-27 21:33:47 +08:00
Yaodong Yu
0a8c2a35fe feat: add TOTP multi-factor authentication (#2014)
* feat: add totp multi-factor authentication

* feat: add license

* feat:i18n and update yarn.lock

* feat:i18n

* fix: i18n
2023-06-24 18:39:54 +08:00
XDTD
d1e734e4ce fix: set the default value of user.Groups for syncer (#2016)
fix: set the default value of user.Groups for syncer
2023-06-24 18:29:50 +08:00
XDTD
68f032b54d fix: add isReadOnly for syncer (#2015)
* feat: add read only mod for syncer

* feat: change readOnlyEnable to isReadOnly
2023-06-24 17:56:41 +08:00
June
1780620ef4 feat: handle error when permission not found (#2012) 2023-06-24 00:30:43 +08:00
Yang Luo
5c968ed1ce Fix avatar cannot show issue 2023-06-23 15:53:41 +08:00
Yang Luo
4016fc0f65 Add EnableChatPages to Conf 2023-06-23 11:35:34 +08:00
June
463b3ad976 fix: refactor and optimize Enforce() API (#2009) 2023-06-22 17:45:24 +08:00
Yang Luo
b817a55f9f Fix error handling in SetPassword() 2023-06-22 14:51:56 +08:00
June
2c2ddfbb92 feat: optimize batch-enforce (#1997) 2023-06-22 14:40:09 +08:00
81 changed files with 2565 additions and 1835 deletions

View File

@@ -50,7 +50,8 @@ func (c *ApiController) GetApplications() {
}
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedApplications(applications, userId)
@@ -59,13 +60,15 @@ func (c *ApiController) GetApplications() {
limit := util.ParseInt(limit)
count, err := object.GetApplicationCount(owner, field, value)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
app, err := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
applications := object.GetMaskedApplications(app, userId)
@@ -85,7 +88,8 @@ func (c *ApiController) GetApplication() {
id := c.Input().Get("id")
app, err := object.GetApplication(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedApplication(app, userId)
@@ -104,7 +108,8 @@ func (c *ApiController) GetUserApplication() {
id := c.Input().Get("id")
user, err := object.GetUser(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
if user == nil {
@@ -114,7 +119,8 @@ func (c *ApiController) GetUserApplication() {
app, err := object.GetApplicationByUser(user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedApplication(app, userId)
@@ -147,7 +153,8 @@ func (c *ApiController) GetOrganizationApplications() {
if limit == "" || page == "" {
applications, err := object.GetOrganizationApplications(owner, organization)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedApplications(applications, userId)

View File

@@ -756,7 +756,8 @@ func (c *ApiController) HandleSamlLogin() {
func (c *ApiController) HandleOfficialAccountEvent() {
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
var data struct {
@@ -766,7 +767,8 @@ func (c *ApiController) HandleOfficialAccountEvent() {
}
err = xml.Unmarshal(respBytes, &data)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
lock.Lock()

View File

@@ -79,7 +79,8 @@ func (c *ApiController) getCurrentUser() *object.User {
} else {
user, err = object.GetUser(userId)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return nil
}
}
return user
@@ -112,7 +113,8 @@ func (c *ApiController) GetSessionApplication() *object.Application {
}
application, err := object.GetApplicationByClientId(clientId.(string))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return nil
}
return application

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetCerts() {
if limit == "" || page == "" {
maskedCerts, err := object.GetMaskedCerts(object.GetCerts(owner))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedCerts
@@ -50,13 +51,15 @@ func (c *ApiController) GetCerts() {
limit := util.ParseInt(limit)
count, err := object.GetCertCount(owner, field, value)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
certs, err := object.GetMaskedCerts(object.GetPaginationCerts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.ResponseOk(certs, paginator.Nums())
@@ -80,7 +83,8 @@ func (c *ApiController) GetGlobleCerts() {
if limit == "" || page == "" {
maskedCerts, err := object.GetMaskedCerts(object.GetGlobleCerts())
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedCerts
@@ -89,13 +93,15 @@ func (c *ApiController) GetGlobleCerts() {
limit := util.ParseInt(limit)
count, err := object.GetGlobalCertsCount(field, value)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
certs, err := object.GetMaskedCerts(object.GetPaginationGlobalCerts(paginator.Offset(), limit, field, value, sortField, sortOrder))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.ResponseOk(certs, paginator.Nums())
@@ -113,7 +119,8 @@ func (c *ApiController) GetCert() {
id := c.Input().Get("id")
cert, err := object.GetCert(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedCert(cert)

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetChats() {
if limit == "" || page == "" {
maskedChats, err := object.GetMaskedChats(object.GetChats(owner))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedChats
@@ -77,7 +78,8 @@ func (c *ApiController) GetChat() {
maskedChat, err := object.GetMaskedChat(object.GetChat(id))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedChat

View File

@@ -44,14 +44,26 @@ func (c *ApiController) Enforce() {
}
if permissionId != "" {
enforceResult, err := object.Enforce(permissionId, &request)
permission, err := object.GetPermission(permissionId)
if err != nil {
c.ResponseError(err.Error())
return
}
res := []bool{}
res = append(res, enforceResult)
if permission == nil {
res = append(res, false)
} else {
enforceResult, err := object.Enforce(permission, &request)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
}
c.ResponseOk(res)
return
}
@@ -76,8 +88,16 @@ func (c *ApiController) Enforce() {
}
res := []bool{}
for _, permission := range permissions {
enforceResult, err := object.Enforce(permission.GetId(), &request)
listPermissionIdMap := object.GroupPermissionsByModelAdapter(permissions)
for _, permissionIds := range listPermissionIdMap {
firstPermission, err := object.GetPermission(permissionIds[0])
if err != nil {
c.ResponseError(err.Error())
return
}
enforceResult, err := object.Enforce(firstPermission, &request, permissionIds...)
if err != nil {
c.ResponseError(err.Error())
return
@@ -85,6 +105,7 @@ func (c *ApiController) Enforce() {
res = append(res, enforceResult)
}
c.ResponseOk(res)
}
@@ -109,14 +130,32 @@ func (c *ApiController) BatchEnforce() {
}
if permissionId != "" {
enforceResult, err := object.BatchEnforce(permissionId, &requests)
permission, err := object.GetPermission(permissionId)
if err != nil {
c.ResponseError(err.Error())
return
}
res := [][]bool{}
res = append(res, enforceResult)
if permission == nil {
l := len(requests)
resRequest := make([]bool, l)
for i := 0; i < l; i++ {
resRequest[i] = false
}
res = append(res, resRequest)
} else {
enforceResult, err := object.BatchEnforce(permission, &requests)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
}
c.ResponseOk(res)
return
}
@@ -135,8 +174,16 @@ func (c *ApiController) BatchEnforce() {
}
res := [][]bool{}
for _, permission := range permissions {
enforceResult, err := object.BatchEnforce(permission.GetId(), &requests)
listPermissionIdMap := object.GroupPermissionsByModelAdapter(permissions)
for _, permissionIds := range listPermissionIdMap {
firstPermission, err := object.GetPermission(permissionIds[0])
if err != nil {
c.ResponseError(err.Error())
return
}
enforceResult, err := object.BatchEnforce(firstPermission, &requests, permissionIds...)
if err != nil {
c.ResponseError(err.Error())
return
@@ -144,6 +191,7 @@ func (c *ApiController) BatchEnforce() {
res = append(res, enforceResult)
}
c.ResponseOk(res)
}

View File

@@ -53,7 +53,8 @@ func (c *ApiController) GetMessages() {
}
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedMessages(messages)
@@ -89,7 +90,8 @@ func (c *ApiController) GetMessage() {
id := c.Input().Get("id")
message, err := object.GetMessage(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedMessage(message)
@@ -100,7 +102,8 @@ func (c *ApiController) ResponseErrorStream(errorText string) {
event := fmt.Sprintf("event: myerror\ndata: %s\n\n", errorText)
_, err := c.Ctx.ResponseWriter.Write([]byte(event))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
}
@@ -196,7 +199,8 @@ func (c *ApiController) GetMessageAnswer() {
event := fmt.Sprintf("event: end\ndata: %s\n\n", "end")
_, err = c.Ctx.ResponseWriter.Write([]byte(event))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
answer := stringBuilder.String()
@@ -204,7 +208,8 @@ func (c *ApiController) GetMessageAnswer() {
message.Text = answer
_, err = object.UpdateMessage(message.GetId(), message)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
}

View File

@@ -17,7 +17,6 @@ package controllers
import (
"net/http"
"github.com/beego/beego"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@@ -58,10 +57,7 @@ func (c *ApiController) MfaSetupInitiate() {
return
}
issuer := beego.AppConfig.String("appname")
accountName := user.GetId()
mfaProps, err := MfaUtil.Initiate(c.Ctx, issuer, accountName)
mfaProps, err := MfaUtil.Initiate(c.Ctx, user.GetId())
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetModels() {
if limit == "" || page == "" {
models, err := object.GetModels(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = models
@@ -77,7 +78,8 @@ func (c *ApiController) GetModel() {
model, err := object.GetModel(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = model

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetOrganizations() {
if limit == "" || page == "" {
maskedOrganizations, err := object.GetMaskedOrganizations(object.GetOrganizations(owner))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedOrganizations

View File

@@ -42,7 +42,8 @@ func (c *ApiController) GetPayments() {
if limit == "" || page == "" {
payments, err := object.GetPayments(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = payments
@@ -51,13 +52,15 @@ func (c *ApiController) GetPayments() {
limit := util.ParseInt(limit)
count, err := object.GetPaymentCount(owner, organization, field, value)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
payments, err := object.GetPaginationPayments(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.ResponseOk(payments, paginator.Nums())
@@ -99,7 +102,8 @@ func (c *ApiController) GetPayment() {
payment, err := object.GetPayment(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = payment
@@ -190,7 +194,8 @@ func (c *ApiController) NotifyPayment() {
}
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
}

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetPermissions() {
if limit == "" || page == "" {
permissions, err := object.GetPermissions(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = permissions
@@ -50,13 +51,15 @@ func (c *ApiController) GetPermissions() {
limit := util.ParseInt(limit)
count, err := object.GetPermissionCount(owner, field, value)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
permissions, err := object.GetPaginationPermissions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.ResponseOk(permissions, paginator.Nums())
@@ -116,7 +119,8 @@ func (c *ApiController) GetPermission() {
permission, err := object.GetPermission(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = permission

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetPlans() {
if limit == "" || page == "" {
plans, err := object.GetPlans(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = plans
@@ -79,13 +80,15 @@ func (c *ApiController) GetPlan() {
plan, err := object.GetPlan(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
if includeOption {
options, err := object.GetPermissionsByRole(plan.Role)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
for _, option := range options {

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetPricings() {
if limit == "" || page == "" {
pricings, err := object.GetPricings(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = pricings
@@ -77,7 +78,8 @@ func (c *ApiController) GetPricing() {
pricing, err := object.GetPricing(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = pricing

View File

@@ -42,7 +42,8 @@ func (c *ApiController) GetProducts() {
if limit == "" || page == "" {
products, err := object.GetProducts(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = products
@@ -78,12 +79,14 @@ func (c *ApiController) GetProduct() {
product, err := object.GetProduct(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
err = object.ExtendProductWithProviders(product)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = product

View File

@@ -46,7 +46,8 @@ func (c *ApiController) GetProviders() {
if limit == "" || page == "" {
providers, err := object.GetProviders(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedProviders(providers, isMaskEnabled))
@@ -92,7 +93,8 @@ func (c *ApiController) GetGlobalProviders() {
if limit == "" || page == "" {
globalProviders, err := object.GetGlobalProviders()
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedProviders(globalProviders, isMaskEnabled))

View File

@@ -46,7 +46,8 @@ func (c *ApiController) GetRecords() {
if limit == "" || page == "" {
records, err := object.GetRecords()
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = records
@@ -84,12 +85,14 @@ func (c *ApiController) GetRecordsByFilter() {
record := &object.Record{}
err := util.JsonToStruct(body, record)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
records, err := object.GetRecordsByField(record)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = records

View File

@@ -53,7 +53,8 @@ func (c *ApiController) GetResources() {
if limit == "" || page == "" {
resources, err := object.GetResources(owner, user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = resources
@@ -86,7 +87,8 @@ func (c *ApiController) GetResource() {
resource, err := object.GetResource(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = resource

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetRoles() {
if limit == "" || page == "" {
roles, err := object.GetRoles(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = roles
@@ -77,7 +78,8 @@ func (c *ApiController) GetRole() {
role, err := object.GetRole(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = role

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetSessions() {
if limit == "" || page == "" {
sessions, err := object.GetSessions(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = sessions
@@ -76,7 +77,8 @@ func (c *ApiController) GetSingleSession() {
session, err := object.GetSingleSession(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = session
@@ -155,7 +157,8 @@ func (c *ApiController) IsSessionDuplicated() {
isUserSessionDuplicated, err := object.IsSessionDuplicated(id, sessionId)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = &Response{Status: "ok", Msg: "", Data: isUserSessionDuplicated}

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetSubscriptions() {
if limit == "" || page == "" {
subscriptions, err := object.GetSubscriptions(owner)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = subscriptions
@@ -77,7 +78,8 @@ func (c *ApiController) GetSubscription() {
subscription, err := object.GetSubscription(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = subscription

View File

@@ -42,7 +42,8 @@ func (c *ApiController) GetSyncers() {
if limit == "" || page == "" {
organizationSyncers, err := object.GetOrganizationSyncers(owner, organization)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = organizationSyncers
@@ -78,7 +79,8 @@ func (c *ApiController) GetSyncer() {
syncer, err := object.GetSyncer(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = syncer

View File

@@ -43,7 +43,8 @@ func (c *ApiController) GetTokens() {
if limit == "" || page == "" {
token, err := object.GetTokens(owner, organization)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = token
@@ -78,7 +79,8 @@ func (c *ApiController) GetToken() {
id := c.Input().Get("id")
token, err := object.GetToken(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = token
@@ -193,7 +195,8 @@ func (c *ApiController) GetOAuthToken() {
host := c.Ctx.Request.Host
oAuthtoken, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = oAuthtoken
@@ -236,7 +239,8 @@ func (c *ApiController) RefreshToken() {
refreshToken2, err := object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = refreshToken2
@@ -276,7 +280,8 @@ func (c *ApiController) IntrospectToken() {
}
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
if application == nil || application.ClientSecret != clientSecret {
@@ -289,7 +294,8 @@ func (c *ApiController) IntrospectToken() {
}
token, err := object.GetTokenByTokenAndApplication(tokenValue, application.Name)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
if token == nil {

View File

@@ -41,7 +41,8 @@ func (c *ApiController) GetGlobalUsers() {
if limit == "" || page == "" {
maskedUsers, err := object.GetMaskedUsers(object.GetGlobalUsers())
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedUsers
@@ -101,7 +102,8 @@ func (c *ApiController) GetUsers() {
maskedUsers, err := object.GetMaskedUsers(object.GetUsers(owner))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedUsers
@@ -153,7 +155,8 @@ func (c *ApiController) GetUser() {
if userId != "" && owner != "" {
userFromUserId, err = object.GetUserByUserId(owner, userId)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
@@ -165,7 +168,8 @@ func (c *ApiController) GetUser() {
organization, err := object.GetOrganization(util.GetId("admin", owner))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
if !organization.IsProfilePublic {
@@ -190,18 +194,21 @@ func (c *ApiController) GetUser() {
}
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
err = object.ExtendUserWithRolesAndPermissions(user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
maskedUser, err := object.GetMaskedUser(user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedUser
@@ -416,6 +423,7 @@ func (c *ApiController) SetPassword() {
requestUserId := c.GetSessionUsername()
if requestUserId == "" && code == "" {
c.ResponseError(c.T("general:Please login first"), "Please login first")
return
} else if code == "" {
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true, c.GetAcceptLanguage())
@@ -425,7 +433,7 @@ func (c *ApiController) SetPassword() {
}
} else {
if code != c.GetSession("verifiedCode") {
c.ResponseError("")
c.ResponseError(c.T("general:Missing parameter"))
return
}
c.SetSession("verifiedCode", "")
@@ -497,7 +505,8 @@ func (c *ApiController) GetSortedUsers() {
maskedUsers, err := object.GetMaskedUsers(object.GetSortedUsers(owner, sorter, limit))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedUsers

View File

@@ -97,7 +97,8 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
user, err := object.GetUser(userId)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return nil, false
}
if user == nil {

View File

@@ -42,7 +42,8 @@ func (c *ApiController) GetWebhooks() {
if limit == "" || page == "" {
webhooks, err := object.GetWebhooks(owner, organization)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = webhooks
@@ -79,7 +80,8 @@ func (c *ApiController) GetWebhook() {
webhook, err := object.GetWebhook(id)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = webhook

4
go.mod
View File

@@ -42,7 +42,7 @@ require (
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5
github.com/pkoukk/tiktoken-go v0.1.1
github.com/plutov/paypal/v4 v4.7.0
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.11.1
github.com/prometheus/client_model v0.2.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
@@ -59,7 +59,7 @@ require (
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/xorm-io/builder v0.3.13 // indirect
github.com/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect

7
go.sum
View File

@@ -105,6 +105,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
@@ -495,10 +497,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
github.com/plutov/paypal/v4 v4.7.0 h1:6TRvYD4ny6yQfHaABeStNf43GFM1wpW5jU/XEDGQmq0=
github.com/plutov/paypal/v4 v4.7.0/go.mod h1:D56boafCRGcF/fEM0w282kj0fCDKIyrwOPX/Te1jCmw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -595,7 +597,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@@ -22,6 +22,8 @@ import (
"github.com/beego/beego/context"
)
const MfaRecoveryCodesSession = "mfa_recovery_codes"
type MfaSessionData struct {
UserId string
}
@@ -37,10 +39,10 @@ type MfaProps struct {
}
type MfaInterface interface {
SetupVerify(ctx *context.Context, passCode string) error
Verify(passCode string) error
Initiate(ctx *context.Context, name1 string, name2 string) (*MfaProps, error)
Initiate(ctx *context.Context, userId string) (*MfaProps, error)
SetupVerify(ctx *context.Context, passcode string) error
Enable(ctx *context.Context, user *User) error
Verify(passcode string) error
}
const (
@@ -58,11 +60,11 @@ const (
func GetMfaUtil(mfaType string, config *MfaProps) MfaInterface {
switch mfaType {
case SmsType:
return NewSmsTwoFactor(config)
return NewSmsMfaUtil(config)
case EmailType:
return NewEmailTwoFactor(config)
return NewEmailMfaUtil(config)
case TotpType:
return nil
return NewTotpMfaUtil(config)
}
return nil
@@ -97,23 +99,9 @@ func MfaRecover(user *User, recoveryCode string) error {
func GetAllMfaProps(user *User, masked bool) []*MfaProps {
mfaProps := []*MfaProps{}
if user.MfaPhoneEnabled {
mfaProps = append(mfaProps, user.GetMfaProps(SmsType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: SmsType,
})
for _, mfaType := range []string{SmsType, EmailType, TotpType} {
mfaProps = append(mfaProps, user.GetMfaProps(mfaType, masked))
}
if user.MfaEmailEnabled {
mfaProps = append(mfaProps, user.GetMfaProps(EmailType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: EmailType,
})
}
return mfaProps
}
@@ -121,6 +109,13 @@ func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps := &MfaProps{}
if mfaType == SmsType {
if !user.MfaPhoneEnabled {
return &MfaProps{
Enabled: false,
MfaType: mfaType,
}
}
mfaProps = &MfaProps{
Enabled: user.MfaPhoneEnabled,
MfaType: mfaType,
@@ -132,6 +127,13 @@ func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps.Secret = user.Phone
}
} else if mfaType == EmailType {
if !user.MfaEmailEnabled {
return &MfaProps{
Enabled: false,
MfaType: mfaType,
}
}
mfaProps = &MfaProps{
Enabled: user.MfaEmailEnabled,
MfaType: mfaType,
@@ -142,9 +144,22 @@ func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps.Secret = user.Email
}
} else if mfaType == TotpType {
if user.TotpSecret == "" {
return &MfaProps{
Enabled: false,
MfaType: mfaType,
}
}
mfaProps = &MfaProps{
Enabled: true,
MfaType: mfaType,
}
if masked {
mfaProps.Secret = ""
} else {
mfaProps.Secret = user.TotpSecret
}
}
if user.PreferredMfaType == mfaType {
@@ -158,8 +173,9 @@ func DisabledMultiFactorAuth(user *User) error {
user.RecoveryCodes = []string{}
user.MfaPhoneEnabled = false
user.MfaEmailEnabled = false
user.TotpSecret = ""
_, err := UpdateUser(user.GetId(), user, []string{"preferred_mfa_type", "recovery_codes", "mfa_phone_enabled", "mfa_email_enabled"}, user.IsAdminUser())
_, err := updateUser(user.GetId(), user, []string{"preferred_mfa_type", "recovery_codes", "mfa_phone_enabled", "mfa_email_enabled", "totp_secret"})
if err != nil {
return err
}

View File

@@ -18,26 +18,24 @@ import (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/util"
"github.com/google/uuid"
)
const (
MfaSmsCountryCodeSession = "mfa_country_code"
MfaSmsDestSession = "mfa_dest"
MfaSmsRecoveryCodesSession = "mfa_recovery_codes"
MfaSmsCountryCodeSession = "mfa_country_code"
MfaSmsDestSession = "mfa_dest"
)
type SmsMfa struct {
Config *MfaProps
}
func (mfa *SmsMfa) Initiate(ctx *context.Context, name string, secret string) (*MfaProps, error) {
func (mfa *SmsMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
recoveryCode := uuid.NewString()
err := ctx.Input.CruSession.Set(MfaSmsRecoveryCodesSession, []string{recoveryCode})
err := ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
if err != nil {
return nil, err
}
@@ -63,9 +61,9 @@ func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
}
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaSmsRecoveryCodesSession).([]string)
recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is empty")
return fmt.Errorf("recovery codes is missing")
}
columns := []string{"recovery_codes", "preferred_mfa_type"}
@@ -111,7 +109,7 @@ func (mfa *SmsMfa) Verify(passCode string) error {
return nil
}
func NewSmsTwoFactor(config *MfaProps) *SmsMfa {
func NewSmsMfaUtil(config *MfaProps) *SmsMfa {
if config == nil {
config = &MfaProps{
MfaType: SmsType,
@@ -122,7 +120,7 @@ func NewSmsTwoFactor(config *MfaProps) *SmsMfa {
}
}
func NewEmailTwoFactor(config *MfaProps) *SmsMfa {
func NewEmailMfaUtil(config *MfaProps) *SmsMfa {
if config == nil {
config = &MfaProps{
MfaType: EmailType,

133
object/mfa_totp.go Normal file
View File

@@ -0,0 +1,133 @@
// 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.
package object
import (
"errors"
"fmt"
"github.com/beego/beego"
"github.com/beego/beego/context"
"github.com/google/uuid"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
const MfaTotpSecretSession = "mfa_totp_secret"
type TotpMfa struct {
Config *MfaProps
period uint
secretSize uint
digits otp.Digits
}
func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
issuer := beego.AppConfig.String("appname")
if issuer == "" {
issuer = "casdoor"
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: issuer,
AccountName: userId,
Period: mfa.period,
SecretSize: mfa.secretSize,
Digits: mfa.digits,
})
if err != nil {
return nil, err
}
err = ctx.Input.CruSession.Set(MfaTotpSecretSession, key.Secret())
if err != nil {
return nil, err
}
recoveryCode := uuid.NewString()
err = ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
if err != nil {
return nil, err
}
mfaProps := MfaProps{
MfaType: mfa.Config.MfaType,
RecoveryCodes: []string{recoveryCode},
Secret: key.Secret(),
URL: key.URL(),
}
return &mfaProps, nil
}
func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string)
result := totp.Validate(passcode, secret)
if result {
return nil
} else {
return errors.New("totp passcode error")
}
}
func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is missing")
}
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string)
if secret == "" {
return fmt.Errorf("totp secret is missing")
}
columns := []string{"recovery_codes", "preferred_mfa_type", "totp_secret"}
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...)
user.TotpSecret = secret
if user.PreferredMfaType == "" {
user.PreferredMfaType = mfa.Config.MfaType
}
_, err := updateUser(user.GetId(), user, columns)
if err != nil {
return err
}
return nil
}
func (mfa *TotpMfa) Verify(passcode string) error {
result := totp.Validate(passcode, mfa.Config.Secret)
if result {
return nil
} else {
return errors.New("totp passcode error")
}
}
func NewTotpMfaUtil(config *MfaProps) *TotpMfa {
if config == nil {
config = &MfaProps{
MfaType: TotpType,
}
}
return &TotpMfa{
Config: config,
period: 30,
secretSize: 20,
digits: otp.DigitsSix,
}
}

View File

@@ -370,3 +370,24 @@ func GetMaskedPermissions(permissions []*Permission) []*Permission {
return permissions
}
// GroupPermissionsByModelAdapter group permissions by model and adapter.
// Every model and adapter will be a key, and the value is a list of permission ids.
// With each list of permission ids have the same key, we just need to init the
// enforcer and do the enforce/batch-enforce once (with list of permission ids
// as the policyFilter when the enforcer load policy).
func GroupPermissionsByModelAdapter(permissions []*Permission) map[string][]string {
m := make(map[string][]string)
for _, permission := range permissions {
key := permission.Model + permission.Adapter
permissionIds, ok := m[key]
if !ok {
m[key] = []string{permission.GetId()}
} else {
m[key] = append(permissionIds, permission.GetId())
}
}
return m
}

View File

@@ -26,7 +26,7 @@ import (
xormadapter "github.com/casdoor/xorm-adapter/v3"
)
func getEnforcer(permission *Permission) *casbin.Enforcer {
func getEnforcer(permission *Permission, permissionIDs ...string) *casbin.Enforcer {
tableName := "permission_rule"
if len(permission.Adapter) != 0 {
adapterObj, err := getCasbinAdapter(permission.Owner, permission.Adapter)
@@ -77,8 +77,13 @@ func getEnforcer(permission *Permission) *casbin.Enforcer {
enforcer.SetAdapter(adapter)
policyFilterV5 := []string{permission.GetId()}
if len(permissionIDs) != 0 {
policyFilterV5 = permissionIDs
}
policyFilter := xormadapter.Filter{
V5: []string{permission.GetId()},
V5: policyFilterV5,
}
if !HasRoleDefinition(m) {
@@ -241,28 +246,13 @@ func removePolicies(permission *Permission) {
type CasbinRequest = []interface{}
func Enforce(permissionId string, request *CasbinRequest) (bool, error) {
permission, err := GetPermission(permissionId)
if err != nil {
return false, err
}
enforcer := getEnforcer(permission)
func Enforce(permission *Permission, request *CasbinRequest, permissionIds ...string) (bool, error) {
enforcer := getEnforcer(permission, permissionIds...)
return enforcer.Enforce(*request...)
}
func BatchEnforce(permissionId string, requests *[]CasbinRequest) ([]bool, error) {
permission, err := GetPermission(permissionId)
if err != nil {
res := []bool{}
for i := 0; i < len(*requests); i++ {
res = append(res, false)
}
return res, err
}
enforcer := getEnforcer(permission)
func BatchEnforce(permission *Permission, requests *[]CasbinRequest, permissionIds ...string) ([]bool, error) {
enforcer := getEnforcer(permission, permissionIds...)
return enforcer.BatchEnforce(*requests)
}

View File

@@ -50,6 +50,7 @@ type Syncer struct {
AvatarBaseUrl string `xorm:"varchar(100)" json:"avatarBaseUrl"`
ErrorText string `xorm:"mediumtext" json:"errorText"`
SyncInterval int `json:"syncInterval"`
IsReadOnly bool `json:"isReadOnly"`
IsEnabled bool `json:"isEnabled"`
Adapter *Adapter `xorm:"-" json:"-"`

View File

@@ -63,9 +63,11 @@ func (syncer *Syncer) syncUsers() {
}
} else {
if user.PreHash == oHash {
updatedOUser := syncer.createOriginalUserFromUser(user)
syncer.updateUser(updatedOUser)
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
if !syncer.IsReadOnly {
updatedOUser := syncer.createOriginalUserFromUser(user)
syncer.updateUser(updatedOUser)
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
}
// update preHash
user.PreHash = user.Hash
@@ -91,15 +93,17 @@ func (syncer *Syncer) syncUsers() {
panic(err)
}
for _, user := range users {
id := user.Id
if _, ok := oUserMap[id]; !ok {
newOUser := syncer.createOriginalUserFromUser(user)
_, err = syncer.addUser(newOUser)
if err != nil {
panic(err)
if !syncer.IsReadOnly {
for _, user := range users {
id := user.Id
if _, ok := oUserMap[id]; !ok {
newOUser := syncer.createOriginalUserFromUser(user)
_, err = syncer.addUser(newOUser)
if err != nil {
panic(err)
}
fmt.Printf("New oUser: %v\n", newOUser)
}
fmt.Printf("New oUser: %v\n", newOUser)
}
}
}

View File

@@ -170,6 +170,7 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
originalUser := &OriginalUser{
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
}
for _, tableColumn := range syncer.TableColumns {

View File

@@ -161,6 +161,7 @@ type User struct {
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret,omitempty"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
@@ -432,16 +433,19 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
if user.AccessSecret != "" {
user.AccessSecret = "***"
}
if user.RecoveryCodes != nil {
user.RecoveryCodes = nil
}
if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts {
manageAccount.Password = "***"
}
}
if user.TotpSecret != "" {
user.TotpSecret = ""
}
if user.RecoveryCodes != nil {
user.RecoveryCodes = nil
}
return user, nil
}

View File

@@ -24,8 +24,6 @@
"i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19",
"moment": "^2.29.1",
"qrcode.react": "^3.1.0",
"qs": "^6.10.2",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1",

View File

@@ -76,6 +76,10 @@ class AdapterEditPage extends React.Component {
getModels(organizationName) {
ModelBackend.getModels(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
models: res,
});

View File

@@ -343,9 +343,11 @@ class App extends Component {
items.push(Setting.getItem(<><SettingOutlined />&nbsp;&nbsp;{i18next.t("account:My Account")}</>,
"/account"
));
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")}</>,
"/logout"));
@@ -411,6 +413,14 @@ class App extends Component {
res.push(Setting.getItem(<Link to="/">{i18next.t("general:Home")}</Link>, "/"));
if (Setting.isLocalAdminUser(this.state.account)) {
if (Conf.ShowGithubCorner) {
res.push(Setting.getItem(<a href={"https://casdoor.com"}>
<span style={{fontWeight: "bold", backgroundColor: "rgba(87,52,211,0.4)", marginTop: "12px", paddingLeft: "5px", paddingRight: "5px", display: "flex", alignItems: "center", height: "40px", borderRadius: "5px"}}>
🚀 SaaS Hosting 🔥
</span>
</a>, "#"));
}
res.push(Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>,
"/organizations"));
@@ -449,13 +459,15 @@ class App extends Component {
"/providers"
));
res.push(Setting.getItem(<Link to="/chats">{i18next.t("general:Chats")}</Link>,
"/chats"
));
if (Conf.EnableChatPages) {
res.push(Setting.getItem(<Link to="/chats">{i18next.t("general:Chats")}</Link>,
"/chats"
));
res.push(Setting.getItem(<Link to="/messages">{i18next.t("general:Messages")}</Link>,
"/messages"
));
res.push(Setting.getItem(<Link to="/messages">{i18next.t("general:Messages")}</Link>,
"/messages"
));
}
res.push(Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>,
"/resources"
@@ -476,7 +488,6 @@ class App extends Component {
res.push(Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>,
"/subscriptions"
));
}
if (Setting.isLocalAdminUser(this.state.account)) {
@@ -501,7 +512,6 @@ class App extends Component {
));
if (Conf.EnableExtraPages) {
res.push(Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>,
"/products"
));
@@ -510,8 +520,8 @@ class App extends Component {
"/payments"
));
}
}
if (Setting.isAdminUser(this.state.account)) {
res.push(Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>,
"/sysinfo"

View File

@@ -118,20 +118,25 @@ class ApplicationEditPage extends React.Component {
getApplication() {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
if (application === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (application.grantTypes === null || application.grantTypes === undefined || application.grantTypes.length === 0) {
application.grantTypes = ["authorization_code"];
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
if (res.grantTypes === null || res.grantTypes === undefined || res.grantTypes.length === 0) {
res.grantTypes = ["authorization_code"];
}
this.setState({
application: application,
application: res,
});
this.getCerts(application.organization);
this.getCerts(res.organization);
});
}

View File

@@ -44,14 +44,19 @@ class CertEditPage extends React.Component {
getCert() {
CertBackend.getCert(this.state.owner, this.state.certName)
.then((cert) => {
if (cert === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
cert: cert,
cert: res,
});
});
}

View File

@@ -40,17 +40,21 @@ class ChatEditPage extends React.Component {
getChat() {
ChatBackend.getChat("admin", this.state.chatName)
.then((chat) => {
if (chat === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
chat: chat,
chat: res,
});
this.getUsers(chat.organization);
this.getUsers(res.organization);
});
}
@@ -66,6 +70,11 @@ class ChatEditPage extends React.Component {
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
});

View File

@@ -21,6 +21,8 @@ export const DefaultLanguage = "en";
export const EnableExtraPages = true;
export const EnableChatPages = true;
export const InitThemeAlgorithm = true;
export const ThemeDefault = {
themeType: "default",

View File

@@ -74,8 +74,12 @@ class EntryPage extends React.Component {
});
ApplicationBackend.getApplication("admin", pricing.application)
.then((application) => {
const themeData = application !== null ? Setting.getThemeData(application.organizationObj, application) : Conf.ThemeDefault;
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
const themeData = res !== null ? Setting.getThemeData(res.organizationObj, res) : Conf.ThemeDefault;
this.props.updataThemeData(themeData);
});
};

View File

@@ -45,17 +45,20 @@ class MessageEditPage extends React.Component {
getMessage() {
MessageBackend.getMessage("admin", this.state.messageName)
.then((message) => {
if (message === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
message: message,
message: res,
});
this.getUsers(message.organization);
this.getUsers(res.organization);
});
}
@@ -80,6 +83,10 @@ class MessageEditPage extends React.Component {
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
});

View File

@@ -47,14 +47,19 @@ class ModelEditPage extends React.Component {
getModel() {
ModelBackend.getModel(this.state.organizationName, this.state.modelName)
.then((model) => {
if (model === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
model: model,
model: res,
});
});
}

View File

@@ -68,9 +68,14 @@ class OrganizationEditPage extends React.Component {
getApplications() {
ApplicationBackend.getApplicationsByOrganization("admin", this.state.organizationName)
.then((applications) => {
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
applications: applications,
applications: res,
});
});
}

View File

@@ -49,21 +49,26 @@ class PermissionEditPage extends React.Component {
getPermission() {
PermissionBackend.getPermission(this.state.organizationName, this.state.permissionName)
.then((permission) => {
if (permission === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
permission: permission,
permission: res,
});
this.getUsers(permission.owner);
this.getRoles(permission.owner);
this.getModels(permission.owner);
this.getResources(permission.owner);
this.getModel(permission.owner, permission.model);
this.getUsers(res.owner);
this.getRoles(res.owner);
this.getModels(res.owner);
this.getResources(res.owner);
this.getModel(res.owner, res.model);
});
}
@@ -79,6 +84,10 @@ class PermissionEditPage extends React.Component {
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
});
@@ -88,6 +97,10 @@ class PermissionEditPage extends React.Component {
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res,
});
@@ -97,6 +110,10 @@ class PermissionEditPage extends React.Component {
getModels(organizationName) {
ModelBackend.getModels(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
models: res,
});
@@ -106,6 +123,10 @@ class PermissionEditPage extends React.Component {
getModel(organizationName, modelName) {
ModelBackend.getModel(organizationName, modelName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
model: res,
});

View File

@@ -64,6 +64,10 @@ class PlanEditPage extends React.Component {
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res,
});
@@ -73,6 +77,10 @@ class PlanEditPage extends React.Component {
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
});

View File

@@ -49,22 +49,31 @@ class PricingEditPage extends React.Component {
getPricing() {
PricingBackend.getPricing(this.state.organizationName, this.state.pricingName)
.then((pricing) => {
if (pricing === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
pricing: pricing,
pricing: res,
});
this.getPlans(pricing.owner);
this.getPlans(res.owner);
});
}
getPlans(organizationName) {
PlanBackend.getPlans(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
plans: res,
});
@@ -109,9 +118,13 @@ class PricingEditPage extends React.Component {
getUserApplication() {
ApplicationBackend.getUserApplication(this.state.organizationName, this.state.userName)
.then((application) => {
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
application: application,
application: res,
});
});
}

View File

@@ -41,9 +41,14 @@ class ProductBuyPage extends React.Component {
}
ProductBackend.getProduct(this.props.account.owner, this.state.productName)
.then((product) => {
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
product: product,
product: res,
});
});
}

View File

@@ -42,18 +42,22 @@ class RoleEditPage extends React.Component {
getRole() {
RoleBackend.getRole(this.state.organizationName, this.state.roleName)
.then((role) => {
if (role === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
role: role,
role: res,
});
this.getUsers(role.owner);
this.getRoles(role.owner);
this.getUsers(res.owner);
this.getRoles(res.owner);
});
}
@@ -69,6 +73,10 @@ class RoleEditPage extends React.Component {
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
});
@@ -78,6 +86,10 @@ class RoleEditPage extends React.Component {
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res,
});

View File

@@ -46,18 +46,23 @@ class SubscriptionEditPage extends React.Component {
getSubscription() {
SubscriptionBackend.getSubscription(this.state.organizationName, this.state.subscriptionName)
.then((subscription) => {
if (subscription === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
subscription: subscription,
subscription: res,
});
this.getUsers(subscription.owner);
this.getPlanes(subscription.owner);
this.getUsers(res.owner);
this.getPlanes(res.owner);
});
}
@@ -73,6 +78,10 @@ class SubscriptionEditPage extends React.Component {
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
});

View File

@@ -47,14 +47,19 @@ class SyncerEditPage extends React.Component {
getSyncer() {
SyncerBackend.getSyncer("admin", this.state.syncerName)
.then((syncer) => {
if (syncer === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
syncer: syncer,
syncer: res,
});
});
}
@@ -376,6 +381,16 @@ class SyncerEditPage extends React.Component {
</div>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("syncer:Is read-only"), i18next.t("syncer:Is read-only - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.syncer.isReadOnly} onChange={checked => {
this.updateSyncerField("isReadOnly", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :

View File

@@ -42,6 +42,7 @@ class SyncerListPage extends BaseListPage {
affiliationTable: "",
avatarBaseUrl: "",
syncInterval: 10,
isReadOnly: false,
isEnabled: false,
};
}

View File

@@ -35,14 +35,19 @@ class TokenEditPage extends React.Component {
getToken() {
TokenBackend.getToken("admin", this.state.tokenName)
.then((token) => {
if (token === null) {
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
token: token,
token: res,
});
});
}

View File

@@ -121,13 +121,17 @@ class UserEditPage extends React.Component {
getUserApplication() {
ApplicationBackend.getUserApplication(this.state.organizationName, this.state.userName)
.then((application) => {
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
application: application,
application: res,
});
this.setState({
isGroupsVisible: application.organizationObj.accountItems?.some((item) => item.name === "Groups" && item.visible),
isGroupsVisible: res.organizationObj.accountItems?.some((item) => item.name === "Groups" && item.visible),
});
});
}

View File

@@ -270,7 +270,7 @@ class UserListPage extends BaseListPage {
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={50} />
<img referrerPolicy="no-referrer" src={text} alt={text} width={50} />
</a>
);
},

View File

@@ -63,8 +63,12 @@ class ForgetPage extends React.Component {
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.onUpdateApplication(res);
});
}
getApplicationObj() {

View File

@@ -49,7 +49,6 @@ class LoginPage extends React.Component {
username: null,
validEmailOrPhone: false,
validEmail: false,
loginMethod: "password",
enableCaptchaModal: CaptchaRule.Never,
openCaptchaModal: false,
verifyCaptcha: undefined,
@@ -83,6 +82,8 @@ class LoginPage extends React.Component {
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.application !== this.props.application) {
this.setState({loginMethod: this.getDefaultLoginMethod(this.props.application)});
const captchaProviderItems = this.getCaptchaProviderItems(this.props.application);
if (captchaProviderItems) {
if (captchaProviderItems.some(providerItem => providerItem.rule === "Always")) {
@@ -159,8 +160,12 @@ class LoginPage extends React.Component {
if (this.state.owner === null || this.state.type === "saml") {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.onUpdateApplication(res);
});
} else {
OrganizationBackend.getDefaultApplication("admin", this.state.owner)
@@ -183,6 +188,20 @@ class LoginPage extends React.Component {
return this.props.application;
}
getDefaultLoginMethod(application) {
if (application.enablePassword) {
return "password";
}
if (application.enableCodeSignin) {
return "verificationCode";
}
if (application.enableWebAuthn) {
return "webAuthn";
}
return "password";
}
onUpdateAccount(account) {
this.props.onUpdateAccount(account);
}
@@ -426,7 +445,8 @@ class LoginPage extends React.Component {
);
}
if (application.enablePassword) {
const showForm = application.enablePassword || application.enableCodeSignin || application.enableWebAuthn;
if (showForm) {
let loginWidth = 320;
if (Setting.getLanguage() === "fr") {
loginWidth += 20;
@@ -511,7 +531,6 @@ class LoginPage extends React.Component {
id="input"
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={(this.state.loginMethod === "verificationCode") ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
disabled={!application.enablePassword}
onChange={e => {
this.setState({
username: e.target.value,
@@ -526,7 +545,7 @@ class LoginPage extends React.Component {
</Row>
<div style={{display: "inline-flex", justifyContent: "space-between", width: "320px", marginBottom: AgreementModal.isAgreementRequired(application) ? "5px" : "25px"}}>
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
<Checkbox style={{float: "left"}} disabled={!application.enablePassword}>
<Checkbox style={{float: "left"}}>
{i18next.t("login:Auto sign in")}
</Checkbox>
</Form.Item>
@@ -540,7 +559,6 @@ class LoginPage extends React.Component {
type="primary"
htmlType="submit"
style={{width: "100%", marginBottom: "5px"}}
disabled={!application.enablePassword}
>
{
this.state.loginMethod === "webAuthn" ? i18next.t("login:Sign in with WebAuthn") :
@@ -801,14 +819,14 @@ class LoginPage extends React.Component {
renderMethodChoiceBox() {
const application = this.getApplicationObj();
const items = [];
items.push({label: i18next.t("general:Password"), key: "password"});
application.enablePassword ? items.push({label: i18next.t("general:Password"), key: "password"}) : null;
application.enableCodeSignin ? items.push({label: i18next.t("login:Verification code"), key: "verificationCode"}) : null;
application.enableWebAuthn ? items.push({label: i18next.t("login:WebAuthn"), key: "webAuthn"}) : null;
if (application.enableCodeSignin || application.enableWebAuthn) {
if (items.length > 1) {
return (
<div>
<Tabs items={items} size={"small"} defaultActiveKey="password" onChange={(key) => {
<Tabs items={items} size={"small"} defaultActiveKey={this.getDefaultLoginMethod(application)} onChange={(key) => {
this.setState({loginMethod: key});
}} centered>
</Tabs>
@@ -933,7 +951,7 @@ class LoginPage extends React.Component {
}
const visibleOAuthProviderItems = (application.providers === null) ? [] : application.providers.filter(providerItem => this.isProviderVisible(providerItem));
if (this.props.preview !== "auto" && !application.enablePassword && visibleOAuthProviderItems.length === 1) {
if (this.props.preview !== "auto" && !application.enablePassword && !application.enableCodeSignin && !application.enableWebAuthn && visibleOAuthProviderItems.length === 1) {
Setting.goToLink(Provider.getAuthUrl(application, visibleOAuthProviderItems[0].provider, "signup"));
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", width: "100%"}}>

View File

@@ -16,8 +16,8 @@ import React, {useState} from "react";
import i18next from "i18next";
import {Button, Input} from "antd";
import * as AuthBackend from "./AuthBackend";
import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm, mfaAuth} from "./MfaVerifyForm";
import {EmailMfaType, RecoveryMfaType, SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaAuth} from "./MfaVerifyForm";
export const NextMfa = "NextMfa";
export const RequiredMfa = "RequiredMfa";
@@ -60,7 +60,7 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
});
};
if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
if (mfaType !== RecoveryMfaType) {
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
@@ -69,12 +69,18 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
<div style={{marginBottom: 24}}>
{i18next.t("mfa:Multi-factor authentication description")}
</div>
<MfaSmsVerifyForm
mfaProps={mfaProps}
method={mfaAuth}
onFinish={verify}
application={application}
/>
{mfaType === SmsMfaType || mfaType === EmailMfaType ? (
<MfaSmsVerifyForm
mfaProps={mfaProps}
method={mfaAuth}
onFinish={verify}
application={application}
/>) : (
<MfaTotpVerifyForm
mfaProps={mfaProps}
onFinish={verify}
/>
)}
<span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")}
<a onClick={() => {
@@ -85,7 +91,7 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
</span>
</div>
);
} else if (mfaType === "recovery") {
} else {
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {useEffect, useState} from "react";
import React, {useState} from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting";
@@ -26,6 +26,7 @@ import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm";
export const EmailMfaType = "email";
export const SmsMfaType = "sms";
export const TotpMfaType = "app";
export const RecoveryMfaType = "recovery";
function CheckPasswordForm({user, onSuccess, onFail}) {
const [form] = Form.useForm();
@@ -76,29 +77,11 @@ function CheckPasswordForm({user, onSuccess, onFail}) {
);
}
export function MfaVerifyForm({mfaType, application, user, onSuccess, onFail}) {
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const [mfaProps, setMfaProps] = useState({mfaType: mfaType});
useEffect(() => {
if (mfaType === SmsMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.phone,
countryCode: user.countryCode,
});
}
if (mfaType === EmailMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.email,
});
}
}, [mfaType]);
const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaType, ...user};
const data = {passcode, mfaType: mfaProps.mfaType, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
@@ -115,14 +98,14 @@ export function MfaVerifyForm({mfaType, application, user, onSuccess, onFail}) {
});
};
if (mfaType === null || mfaType === undefined || mfaProps.secret === undefined) {
if (mfaProps === undefined || mfaProps === null) {
return <div></div>;
}
if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} method={mfaSetup} mfaProps={mfaProps} />;
} else if (mfaType === TotpMfaType) {
return <MfaTotpVerifyForm onFinish={onFinish} />;
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>;
}
@@ -183,7 +166,7 @@ class MfaSetupPage extends React.Component {
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.isAuthenticated === true && this.state.mfaProps === null) {
if (this.state.isAuthenticated === true && (this.state.mfaProps === null || this.state.mfaType !== prevState.mfaType)) {
MfaBackend.MfaSetupInitiate({
mfaType: this.state.mfaType,
...this.getUser(),
@@ -205,10 +188,14 @@ class MfaSetupPage extends React.Component {
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
if (application !== null) {
.then((res) => {
if (res !== null) {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
application: application,
application: res,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to get application"));
@@ -226,18 +213,20 @@ class MfaSetupPage extends React.Component {
renderStep() {
switch (this.state.current) {
case 0:
return <CheckPasswordForm
user={this.getUser()}
onSuccess={() => {
this.setState({
current: this.state.current + 1,
isAuthenticated: true,
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}}
/>;
return (
<CheckPasswordForm
user={this.getUser()}
onSuccess={() => {
this.setState({
current: this.state.current + 1,
isAuthenticated: true,
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}}
/>
);
case 1:
if (!this.state.isAuthenticated) {
return null;
@@ -246,7 +235,7 @@ class MfaSetupPage extends React.Component {
return (
<div>
<MfaVerifyForm
mfaType={this.state.mfaType}
mfaProps={this.state.mfaProps}
application={this.state.application}
user={this.props.account}
onSuccess={() => {
@@ -261,11 +250,6 @@ class MfaSetupPage extends React.Component {
<Col span={24} style={{display: "flex", justifyContent: "left"}}>
{(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null :
<Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${EmailMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${EmailMfaType}`);
}
this.setState({
mfaType: EmailMfaType,
});
@@ -275,17 +259,21 @@ class MfaSetupPage extends React.Component {
{
(this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null :
<Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${SmsMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${SmsMfaType}`);
}
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>
</div>
);
@@ -294,18 +282,20 @@ class MfaSetupPage extends React.Component {
return null;
}
return <EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
if (this.state.isPromptPage && this.state.redirectUri) {
Setting.goToLink(this.state.redirectUri);
} else {
Setting.goToLink("/account");
}
}}
onFail={(res) => {
Setting.showMessage("error", `${i18next.t("general:Failed to enable")}: ${res.msg}`);
}} />;
return (
<EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
if (this.state.isPromptPage && this.state.redirectUri) {
Setting.goToLink(this.state.redirectUri);
} else {
Setting.goToLink("/account");
}
}}
onFail={(res) => {
Setting.showMessage("error", `${i18next.t("general:Failed to enable")}: ${res.msg}`);
}} />
);
default:
return null;
}
@@ -328,24 +318,20 @@ class MfaSetupPage extends React.Component {
<Col span={24} style={{justifyContent: "center"}}>
<Row>
<Col span={24}>
<div style={{textAlign: "center", fontSize: "28px"}}>
{i18next.t("mfa:Protect your account with Multi-factor authentication")}</div>
<div style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</div>
</Col>
</Row>
<Row>
<Col span={24}>
<Steps current={this.state.current}
items={[
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
]}
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "80px",
}} >
</Steps>
<p style={{textAlign: "center", fontSize: "28px"}}>
{i18next.t("mfa:Protect your account with Multi-factor authentication")}</p>
<p style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</p>
</Col>
</Row>
<Steps current={this.state.current}
items={[
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
]}
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "50px",
}} >
</Steps>
</Col>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "10px", textAlign: "center"}}>

View File

@@ -12,23 +12,33 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Form, Input, Row} from "antd";
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 from "react";
import React, {useEffect} from "react";
import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import {EmailMfaType} from "./MfaSetupPage";
import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
export const mfaAuth = "mfaAuth";
export const mfaSetup = "mfaSetup";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
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;
};
@@ -42,9 +52,9 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
countryCode: mfaProps.countryCode,
}}
>
{mfaProps.secret !== "" ?
{dest !== "" ?
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {mfaProps.secret}
{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") :
@@ -114,44 +124,49 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
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}
>
<Row type="flex" justify="center" align="middle">
<Col>
</Col>
</Row>
<Row type="flex" justify="center" align="middle">
<Col>
{Setting.getLabel(
i18next.t("mfa:Multi-factor secret"),
i18next.t("mfa:Multi-factor secret - Tooltip")
)}
:
</Col>
<Col>
<Input value={mfaProps.secret} />
</Col>
<Col>
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Col>
</Row>
{renderSecret()}
<Form.Item
name="passcode"
rules={[{required: true, message: "Please input your passcode"}]}
@@ -162,7 +177,6 @@ export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => {
placeholder={i18next.t("mfa:Passcode")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}

View File

@@ -49,9 +49,14 @@ class PromptPage extends React.Component {
const organizationName = this.props.account.owner;
const userName = this.props.account.name;
UserBackend.getUser(organizationName, userName)
.then((user) => {
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
user: user,
user: res,
});
});
}
@@ -62,10 +67,15 @@ class PromptPage extends React.Component {
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.onUpdateApplication(res);
this.setState({
application: application,
application: res,
});
});
}

View File

@@ -43,10 +43,14 @@ class ResultPage extends React.Component {
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.onUpdateApplication(res);
this.setState({
application: application,
application: res,
});
});
}

View File

@@ -108,8 +108,13 @@ class SignupPage extends React.Component {
}
ApplicationBackend.getApplication("admin", applicationName)
.then((application) => {
this.onUpdateApplication(application);
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.onUpdateApplication(res);
});
}

View File

@@ -127,6 +127,11 @@ export const CropperDivModal = (props) => {
setLoading(true);
ResourceBackend.getResources(user.owner, user.name, "", "", "", "", "", "")
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
setLoading(false);
return;
}
setLoading(false);
setOptions(getOptions(res));
});

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Preisseite URL kopieren",
"Edit Pricing": "Edit Pricing",
"Free": "Kostenlos",
"Failed to get plans": "Es konnten keine Pläne abgerufen werden",
"Getting started": "Loslegen",
"New Pricing": "New Pricing",
"Trial duration": "Testphase Dauer",
@@ -819,6 +821,8 @@
"Error text": "Fehlermeldung",
"Error text - Tooltip": "Fehler Text",
"Is hashed": "ist gehasht",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "Neuer Syncer",
"Sync interval": "Synchronisierungsintervall",
"Sync interval - Tooltip": "Einheit in Sekunden",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"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 secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Copy pricing page URL",
"Edit Pricing": "Edit Pricing",
"Free": "Free",
"Failed to get plans": "Failed to get plans",
"Getting started": "Getting started",
"New Pricing": "New Pricing",
"Trial duration": "Trial duration",
@@ -819,6 +821,8 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text",
"Is hashed": "Is hashed",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "New Syncer",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Unit in seconds",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Copiar URL de la página de precios",
"Edit Pricing": "Edit Pricing",
"Free": "Gratis",
"Failed to get plans": "No se pudieron obtener los planes",
"Getting started": "Empezar",
"New Pricing": "New Pricing",
"Trial duration": "Duración del período de prueba",
@@ -819,6 +821,8 @@
"Error text": "Texto de error",
"Error text - Tooltip": "Texto de error",
"Is hashed": "Está encriptado",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "Nuevo Syncer",
"Sync interval": "Intervalo de sincronización",
"Sync interval - Tooltip": "Unidad en segundos",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Copier l'URL de la page tarifs",
"Edit Pricing": "Edit Pricing",
"Free": "Gratuit",
"Failed to get plans": "Échec de l'obtention des plans",
"Getting started": "Commencer",
"New Pricing": "New Pricing",
"Trial duration": "Durée de l'essai",
@@ -819,6 +821,8 @@
"Error text": "Texte d'erreur",
"Error text - Tooltip": "Texte d'erreur",
"Is hashed": "Est-haché",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "Nouveau synchroniseur",
"Sync interval": "Intervalle de synchronisation",
"Sync interval - Tooltip": "Unité en secondes",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Salin URL halaman harga",
"Edit Pricing": "Edit Pricing",
"Free": "Gratis",
"Failed to get plans": "Gagal mendapatkan rencana",
"Getting started": "Mulai",
"New Pricing": "New Pricing",
"Trial duration": "Durasi percobaan",
@@ -819,6 +821,8 @@
"Error text": "Teks kesalahan",
"Error text - Tooltip": "Teks kesalahan",
"Is hashed": "Apakah di-hash?",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "Sinkronisasi Baru",
"Sync interval": "Interval sinkronisasi",
"Sync interval - Tooltip": "Satuan dalam detik",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "価格ページのURLをコピー",
"Edit Pricing": "Edit Pricing",
"Free": "無料",
"Failed to get plans": "計画の取得に失敗しました",
"Getting started": "はじめる",
"New Pricing": "New Pricing",
"Trial duration": "トライアル期間の長さ",
@@ -819,6 +821,8 @@
"Error text": "エラーテキスト",
"Error text - Tooltip": "エラーテキスト",
"Is hashed": "ハッシュ化されました",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "新しいシンクロナイザー",
"Sync interval": "同期の間隔",
"Sync interval - Tooltip": "単位は秒です",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "가격 페이지 URL 복사",
"Edit Pricing": "Edit Pricing",
"Free": "무료",
"Failed to get plans": "계획을 가져오지 못했습니다.",
"Getting started": "시작하기",
"New Pricing": "New Pricing",
"Trial duration": "체험 기간",
@@ -819,6 +821,8 @@
"Error text": "오류 메시지",
"Error text - Tooltip": "에러 텍스트",
"Is hashed": "해시화 되었습니다",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "신규 싱크어",
"Sync interval": "동기화 간격",
"Sync interval - Tooltip": "초 단위의 단위",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Métodos de vários fatores",
"Multi-factor recover": "Recuperação de vários fatores",
"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 secret": "Segredo de vários fatores",
"Multi-factor secret - Tooltip": "Segredo de vários fatores - Dica de ferramenta",
"Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Código de acesso",
"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 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",
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores",
"Recovery code": "Código de recuperação",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Definir preferido",
"Setup": "Configuração",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Usar código de verificação SMS",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Sao chép URL trang bảng giá",
"Edit Pricing": "Edit Pricing",
"Free": "Miễn phí",
"Failed to get plans": "Falha ao obter planos",
"Getting started": "Bắt đầu",
"New Pricing": "New Pricing",
"Trial duration": "Thời gian thử nghiệm",
@@ -819,6 +821,8 @@
"Error text": "Texto de erro",
"Error text - Tooltip": "Texto de erro",
"Is hashed": "Está criptografado",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "Novo Syncer",
"Sync interval": "Intervalo de sincronização",
"Sync interval - Tooltip": "Unidade em segundos",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Скопировать URL прайс-листа",
"Edit Pricing": "Edit Pricing",
"Free": "Бесплатно",
"Failed to get plans": "Не удалось получить планы",
"Getting started": "Выьрать план",
"New Pricing": "New Pricing",
"Trial duration": "Продолжительность пробного периода",
@@ -819,6 +821,8 @@
"Error text": "Текст ошибки",
"Error text - Tooltip": "Текст ошибки",
"Is hashed": "Хешировано",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "Новый синхронизатор",
"Sync interval": "Интервал синхронизации",
"Sync interval - Tooltip": "Единица измерения в секундах",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"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",
"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 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": "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": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "Sao chép URL trang bảng giá",
"Edit Pricing": "Edit Pricing",
"Free": "Miễn phí",
"Failed to get plans": "Không thể lấy được các kế hoạch",
"Getting started": "Bắt đầu",
"New Pricing": "New Pricing",
"Trial duration": "Thời gian thử nghiệm",
@@ -819,6 +821,8 @@
"Error text": "Văn bản lỗi",
"Error text - Tooltip": "Văn bản lỗi",
"Is hashed": "Đã được băm mã hóa",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "New Syncer: Đồng bộ mới",
"Sync interval": "Khoảng thời gian đồng bộ hóa",
"Sync interval - Tooltip": "Đơn vị giây",

View File

@@ -434,17 +434,18 @@
"Multi-factor methods": "多因素认证方式",
"Multi-factor recover": "重置多因素认证",
"Multi-factor recover description": "如果您无法访问您的设备,输入您的多因素认证恢复代码来确认您的身份",
"Multi-factor secret": "多因素密钥",
"Multi-factor secret - Tooltip": "多因素密钥 - Tooltip",
"Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板",
"Or copy the secret to your Authenticator App": "或者将这个密钥复制到你的身份验证应用中",
"Passcode": "认证码",
"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 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": "通过多因素认证保护您的帐户",
"Recovery code": "恢复码",
"Scan the QR code with your Authenticator App": "用你的身份验证应用扫描二维码",
"Set preferred": "设为首选",
"Setup": "设置",
"Use Authenticator App": "使用身份验证应用",
"Use Email": "使用电子邮件",
"Use SMS": "使用短信",
"Use SMS verification code": "使用手机或电子邮件发送验证码认证",
@@ -568,6 +569,7 @@
"Copy pricing page URL": "复制定价页面链接",
"Edit Pricing": "编辑定价",
"Free": "免费",
"Failed to get plans": "未能获取计划",
"Getting started": "开始使用",
"New Pricing": "添加定价",
"Trial duration": "试用期时长",
@@ -819,6 +821,8 @@
"Error text": "错误信息",
"Error text - Tooltip": "错误信息",
"Is hashed": "是否参与哈希计算",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"New Syncer": "添加同步器",
"Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为秒",

View File

@@ -64,6 +64,11 @@ class PricingPage extends React.Component {
Promise.all(plans)
.then(results => {
const hasError = results.some(result => result.status === "error");
if (hasError) {
Setting.showMessage("error", `${i18next.t("Failed to get plans")}`);
return;
}
this.setState({
plans: results,
loading: false,
@@ -81,6 +86,11 @@ class PricingPage extends React.Component {
PricingBackend.getPricing(this.state.owner, pricingName)
.then((result) => {
if (result.status === "error") {
Setting.showMessage("error", result.msg);
return;
}
this.setState({
loading: false,
pricing: result,

File diff suppressed because it is too large Load Diff