Compare commits

..

15 Commits

Author SHA1 Message Date
ffc0a0e0d5 fix: refresh_token endpoint does not work (#410)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-01 15:20:49 +08:00
ff22bf507f Add role page. 2022-01-01 15:11:16 +08:00
2d4103d751 Add isUserExtended to webhook. 2022-01-01 11:16:37 +08:00
4611b59b08 Add webhook edit page's preview. 2022-01-01 10:58:39 +08:00
445d3c9d0e feat: support spring security oauth2 (#408)
Signed-off-by: abingcbc <abingcbc626@gmail.com>
2021-12-31 19:55:34 +08:00
dbebd1846f Fix code sign-in link hiding. 2021-12-31 13:36:10 +08:00
2fcc8f5bfe Support app user in SetPassword(). 2021-12-31 13:32:18 +08:00
4b65320a96 Support user uploading via xlsx. 2021-12-31 13:00:35 +08:00
5e8897e41b Make cert work. 2021-12-31 10:02:06 +08:00
ba1646a0c3 Add cert pages. 2021-12-31 00:36:36 +08:00
c1cd187558 Improve UI. 2021-12-29 20:50:49 +08:00
519fd655cf Add GetMaskedApplication() and GetMaskedApplications(). 2021-12-29 20:04:39 +08:00
377ac05928 Don't clear session in SetPassword(). 2021-12-28 23:07:09 +08:00
4f124ff140 fix: refresh token does not return (#401)
Signed-off-by: 0x2a <stevesough@gmail.com>
2021-12-28 19:44:17 +08:00
d5f802ec7d Support IdCard in signup page. 2021-12-28 17:48:24 +08:00
60 changed files with 2596 additions and 174 deletions

View File

@ -80,6 +80,7 @@ p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, * p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/get-account, *, * p, *, *, GET, /api/get-account, *, *
p, *, *, POST, /api/login/oauth/access_token, *, * p, *, *, POST, /api/login/oauth/access_token, *, *
p, *, *, POST, /api/login/oauth/refresh_token, *, *
p, *, *, GET, /api/get-application, *, * p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-users, *, * p, *, *, GET, /api/get-users, *, *
p, *, *, GET, /api/get-user, *, * p, *, *, GET, /api/get-user, *, *

View File

@ -38,6 +38,7 @@ type RequestForm struct {
Email string `json:"email"` Email string `json:"email"`
Phone string `json:"phone"` Phone string `json:"phone"`
Affiliation string `json:"affiliation"` Affiliation string `json:"affiliation"`
IdCard string `json:"idCard"`
Region string `json:"region"` Region string `json:"region"`
Application string `json:"application"` Application string `json:"application"`
@ -61,6 +62,7 @@ type Response struct {
Status string `json:"status"` Status string `json:"status"`
Msg string `json:"msg"` Msg string `json:"msg"`
Sub string `json:"sub"` Sub string `json:"sub"`
Name string `json:"name"`
Data interface{} `json:"data"` Data interface{} `json:"data"`
Data2 interface{} `json:"data2"` Data2 interface{} `json:"data2"`
} }
@ -151,6 +153,7 @@ func (c *ApiController) Signup() {
Phone: form.Phone, Phone: form.Phone,
Address: []string{}, Address: []string{},
Affiliation: form.Affiliation, Affiliation: form.Affiliation,
IdCard: form.IdCard,
Region: form.Region, Region: form.Region,
Score: getInitScore(), Score: getInitScore(),
IsAdmin: false, IsAdmin: false,
@ -220,6 +223,7 @@ func (c *ApiController) GetAccount() {
resp := Response{ resp := Response{
Status: "ok", Status: "ok",
Sub: user.Id, Sub: user.Id,
Name: user.Name,
Data: user, Data: user,
Data2: organization, Data2: organization,
} }

View File

@ -30,6 +30,7 @@ import (
// @Success 200 {array} object.Application The Response object // @Success 200 {array} object.Application The Response object
// @router /get-applications [get] // @router /get-applications [get]
func (c *ApiController) GetApplications() { func (c *ApiController) GetApplications() {
userId := c.GetSessionUsername()
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize") limit := c.Input().Get("pageSize")
page := c.Input().Get("p") page := c.Input().Get("p")
@ -38,12 +39,12 @@ func (c *ApiController) GetApplications() {
sortField := c.Input().Get("sortField") sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder") sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" { if limit == "" || page == "" {
c.Data["json"] = object.GetApplications(owner) c.Data["json"] = object.GetMaskedApplications(object.GetApplications(owner), userId)
c.ServeJSON() c.ServeJSON()
} else { } else {
limit := util.ParseInt(limit) limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetApplicationCount(owner, field, value))) paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetApplicationCount(owner, field, value)))
applications := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder) applications := object.GetMaskedApplications(object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder), userId)
c.ResponseOk(applications, paginator.Nums()) c.ResponseOk(applications, paginator.Nums())
} }
} }
@ -56,9 +57,10 @@ func (c *ApiController) GetApplications() {
// @Success 200 {object} object.Application The Response object // @Success 200 {object} object.Application The Response object
// @router /get-application [get] // @router /get-application [get]
func (c *ApiController) GetApplication() { func (c *ApiController) GetApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id") id := c.Input().Get("id")
c.Data["json"] = object.GetApplication(id) c.Data["json"] = object.GetMaskedApplication(object.GetApplication(id), userId)
c.ServeJSON() c.ServeJSON()
} }
@ -70,6 +72,7 @@ func (c *ApiController) GetApplication() {
// @Success 200 {object} object.Application The Response object // @Success 200 {object} object.Application The Response object
// @router /get-user-application [get] // @router /get-user-application [get]
func (c *ApiController) GetUserApplication() { func (c *ApiController) GetUserApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id") id := c.Input().Get("id")
user := object.GetUser(id) user := object.GetUser(id)
if user == nil { if user == nil {
@ -77,7 +80,7 @@ func (c *ApiController) GetUserApplication() {
return return
} }
c.Data["json"] = object.GetApplicationByUser(user) c.Data["json"] = object.GetMaskedApplication(object.GetApplicationByUser(user), userId)
c.ServeJSON() c.ServeJSON()
} }

116
controllers/cert.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
// GetCerts
// @Title GetCerts
// @Tag Cert API
// @Description get certs
// @Param owner query string true "The owner of certs"
// @Success 200 {array} object.Cert The Response object
// @router /get-certs [get]
func (c *ApiController) GetCerts() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedCerts(object.GetCerts(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetCertCount(owner, field, value)))
certs := object.GetMaskedCerts(object.GetPaginationCerts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(certs, paginator.Nums())
}
}
// @Title GetCert
// @Tag Cert API
// @Description get cert
// @Param id query string true "The id of the cert"
// @Success 200 {object} object.Cert The Response object
// @router /get-cert [get]
func (c *ApiController) GetCert() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedCert(object.GetCert(id))
c.ServeJSON()
}
// @Title UpdateCert
// @Tag Cert API
// @Description update cert
// @Param id query string true "The id of the cert"
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /update-cert [post]
func (c *ApiController) UpdateCert() {
id := c.Input().Get("id")
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateCert(id, &cert))
c.ServeJSON()
}
// @Title AddCert
// @Tag Cert API
// @Description add cert
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /add-cert [post]
func (c *ApiController) AddCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddCert(&cert))
c.ServeJSON()
}
// @Title DeleteCert
// @Tag Cert API
// @Description delete cert
// @Param body body object.Cert true "The details of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-cert [post]
func (c *ApiController) DeleteCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteCert(&cert))
c.ServeJSON()
}

View File

@ -28,7 +28,7 @@ func (c *RootController) GetOidcDiscovery() {
// @Tag OIDC API // @Tag OIDC API
// @router /api/certs [get] // @router /api/certs [get]
func (c *RootController) GetOidcCert() { func (c *RootController) GetOidcCert() {
jwks, err := object.GetJSONWebKeySet() jwks, err := object.GetJsonWebKeySet()
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

116
controllers/role.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
// GetRoles
// @Title GetRoles
// @Tag Role API
// @Description get roles
// @Param owner query string true "The owner of roles"
// @Success 200 {array} object.Role The Response object
// @router /get-roles [get]
func (c *ApiController) GetRoles() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetRoles(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetRoleCount(owner, field, value)))
roles := object.GetPaginationRoles(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(roles, paginator.Nums())
}
}
// @Title GetRole
// @Tag Role API
// @Description get role
// @Param id query string true "The id of the role"
// @Success 200 {object} object.Role The Response object
// @router /get-role [get]
func (c *ApiController) GetRole() {
id := c.Input().Get("id")
c.Data["json"] = object.GetRole(id)
c.ServeJSON()
}
// @Title UpdateRole
// @Tag Role API
// @Description update role
// @Param id query string true "The id of the role"
// @Param body body object.Role true "The details of the role"
// @Success 200 {object} controllers.Response The Response object
// @router /update-role [post]
func (c *ApiController) UpdateRole() {
id := c.Input().Get("id")
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateRole(id, &role))
c.ServeJSON()
}
// @Title AddRole
// @Tag Role API
// @Description add role
// @Param body body object.Role true "The details of the role"
// @Success 200 {object} controllers.Response The Response object
// @router /add-role [post]
func (c *ApiController) AddRole() {
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddRole(&role))
c.ServeJSON()
}
// @Title DeleteRole
// @Tag Role API
// @Description delete role
// @Param body body object.Role true "The details of the role"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-role [post]
func (c *ApiController) DeleteRole() {
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteRole(&role))
c.ServeJSON()
}

View File

@ -226,11 +226,6 @@ func (c *ApiController) SetPassword() {
c.ResponseError("Please login first.") c.ResponseError("Please login first.")
return return
} }
requestUser := object.GetUser(requestUserId)
if requestUser == nil {
c.ResponseError("Session outdated. Please login again.")
return
}
userId := fmt.Sprintf("%s/%s", userOwner, userName) userId := fmt.Sprintf("%s/%s", userOwner, userName)
targetUser := object.GetUser(userId) targetUser := object.GetUser(userId)
@ -240,15 +235,22 @@ func (c *ApiController) SetPassword() {
} }
hasPermission := false hasPermission := false
if strings.HasPrefix(requestUserId, "app/") {
if requestUser.IsGlobalAdmin {
hasPermission = true
} else if requestUserId == userId {
hasPermission = true
} else if targetUser.Owner == requestUser.Owner && requestUser.IsAdmin {
hasPermission = true hasPermission = true
} else {
requestUser := object.GetUser(requestUserId)
if requestUser == nil {
c.ResponseError("Session outdated. Please login again.")
return
}
if requestUser.IsGlobalAdmin {
hasPermission = true
} else if requestUserId == userId {
hasPermission = true
} else if targetUser.Owner == requestUser.Owner && requestUser.IsAdmin {
hasPermission = true
}
} }
if !hasPermission { if !hasPermission {
c.ResponseError("You don't have the permission to do this.") c.ResponseError("You don't have the permission to do this.")
return return
@ -272,8 +274,6 @@ func (c *ApiController) SetPassword() {
return return
} }
c.SetSessionUsername("")
targetUser.Password = newPassword targetUser.Password = newPassword
object.SetUserField(targetUser, "password", targetUser.Password) object.SetUserField(targetUser, "password", targetUser.Password)
c.Data["json"] = Response{Status: "ok"} c.Data["json"] = Response{Status: "ok"}

View File

@ -0,0 +1,60 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"fmt"
"io"
"mime/multipart"
"os"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
func saveFile(path string, file *multipart.File) {
f, err := os.Create(path)
if err != nil {
panic(err)
}
defer f.Close()
_, err = io.Copy(f, *file)
if err != nil {
panic(err)
}
}
func (c *ApiController) UploadUsers() {
userId := c.GetSessionUsername()
owner, user := util.GetOwnerAndNameFromId(userId)
file, header, err := c.Ctx.Request.FormFile("file")
if err != nil {
panic(err)
}
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId)
util.EnsureFileFolderExists(path)
saveFile(path, &file)
affected := object.UploadUsers(owner, fileId)
if affected {
c.ResponseOk()
} else {
c.ResponseError("Failed to import users")
}
}

1
go.mod
View File

@ -25,6 +25,7 @@ require (
github.com/russellhaering/goxmldsig v1.1.1 github.com/russellhaering/goxmldsig v1.1.1
github.com/satori/go.uuid v1.2.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4 github.com/thanhpk/randstr v1.0.4
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2

2
go.sum
View File

@ -349,6 +349,8 @@ github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2K
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/tencentcloud/tencentcloud-sdk-go v1.0.154 h1:THBgwGwUQtsw6L53cSSA2wwL3sLrm+HJ3Dk+ye/lMCI= github.com/tencentcloud/tencentcloud-sdk-go v1.0.154 h1:THBgwGwUQtsw6L53cSSA2wwL3sLrm+HJ3Dk+ye/lMCI=
github.com/tencentcloud/tencentcloud-sdk-go v1.0.154/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI= github.com/tencentcloud/tencentcloud-sdk-go v1.0.154/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI=
github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo=

View File

@ -105,6 +105,11 @@ func (a *Adapter) createTable() {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(Role))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Provider)) err = a.Engine.Sync2(new(Provider))
if err != nil { if err != nil {
panic(err) panic(err)
@ -145,6 +150,11 @@ func (a *Adapter) createTable() {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(Cert))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Ldap)) err = a.Engine.Sync2(new(Ldap))
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -31,6 +31,7 @@ type Application struct {
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"` HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
Description string `xorm:"varchar(100)" json:"description"` Description string `xorm:"varchar(100)" json:"description"`
Organization string `xorm:"varchar(100)" json:"organization"` Organization string `xorm:"varchar(100)" json:"organization"`
Cert string `xorm:"varchar(100)" json:"cert"`
EnablePassword bool `json:"enablePassword"` EnablePassword bool `json:"enablePassword"`
EnableSignUp bool `json:"enableSignUp"` EnableSignUp bool `json:"enableSignUp"`
EnableSigninSession bool `json:"enableSigninSession"` EnableSigninSession bool `json:"enableSigninSession"`
@ -200,24 +201,37 @@ func GetApplicationByClientId(clientId string) *Application {
} }
} }
func GetApplicationByClientIdAndSecret(clientId, clientSecret string) *Application {
if util.IsStrsEmpty(clientId, clientSecret) {
return nil
}
app := GetApplicationByClientId(clientId)
if app == nil || app.ClientSecret != clientSecret {
return nil
}
return app
}
func GetApplication(id string) *Application { func GetApplication(id string) *Application {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
return getApplication(owner, name) return getApplication(owner, name)
} }
func GetMaskedApplication(application *Application, userId string) *Application {
if isUserIdGlobalAdmin(userId) {
return application
}
if application == nil {
return nil
}
if application.ClientSecret != "" {
application.ClientSecret = "***"
}
return application
}
func GetMaskedApplications(applications []*Application, userId string) []*Application {
if isUserIdGlobalAdmin(userId) {
return applications
}
for _, application := range applications {
application = GetMaskedApplication(application, userId)
}
return applications
}
func UpdateApplication(id string, application *Application) bool { func UpdateApplication(id string, application *Application) bool {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if getApplication(owner, name) == nil { if getApplication(owner, name) == nil {

164
object/cert.go Normal file
View File

@ -0,0 +1,164 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casbin/casdoor/util"
"xorm.io/core"
)
type Cert struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Scope string `xorm:"varchar(100)" json:"scope"`
Type string `xorm:"varchar(100)" json:"type"`
CryptoAlgorithm string `xorm:"varchar(100)" json:"cryptoAlgorithm"`
BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"`
PublicKey string `xorm:"mediumtext" json:"publicKey"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
}
func GetMaskedCert(cert *Cert) *Cert {
if cert == nil {
return nil
}
return cert
}
func GetMaskedCerts(certs []*Cert) []*Cert {
for _, cert := range certs {
cert = GetMaskedCert(cert)
}
return certs
}
func GetCertCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
count, err := session.Count(&Cert{})
if err != nil {
panic(err)
}
return int(count)
}
func GetCerts(owner string) []*Cert {
certs := []*Cert{}
err := adapter.Engine.Desc("created_time").Find(&certs, &Cert{Owner: owner})
if err != nil {
panic(err)
}
return certs
}
func GetPaginationCerts(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Cert {
certs := []*Cert{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&certs)
if err != nil {
panic(err)
}
return certs
}
func getCert(owner string, name string) *Cert {
if owner == "" || name == "" {
return nil
}
cert := Cert{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&cert)
if err != nil {
panic(err)
}
if existed {
return &cert
} else {
return nil
}
}
func GetCert(id string) *Cert {
owner, name := util.GetOwnerAndNameFromId(id)
return getCert(owner, name)
}
func UpdateCert(id string, cert *Cert) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getCert(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(cert)
if err != nil {
panic(err)
}
return affected != 0
}
func AddCert(cert *Cert) bool {
if cert.PublicKey == "" || cert.PrivateKey == "" {
publicKey, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
cert.PublicKey = publicKey
cert.PrivateKey = privateKey
}
affected, err := adapter.Engine.Insert(cert)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteCert(cert *Cert) bool {
affected, err := adapter.Engine.ID(core.PK{cert.Owner, cert.Name}).Delete(&Cert{})
if err != nil {
panic(err)
}
return affected != 0
}
func (p *Cert) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}
func getCertByApplication(application *Application) *Cert {
if application.Cert != "" {
return getCert("admin", application.Cert)
} else {
return GetDefaultCert()
}
}
func GetDefaultCert() *Cert {
return getCert("admin", "cert-built-in")
}

View File

@ -14,12 +14,23 @@
package object package object
import "github.com/casbin/casdoor/util" import (
_ "embed"
"github.com/casbin/casdoor/util"
)
//go:embed token_jwt_key.pem
var tokenJwtPublicKey string
//go:embed token_jwt_key.key
var tokenJwtPrivateKey string
func InitDb() { func InitDb() {
initBuiltInOrganization() initBuiltInOrganization()
initBuiltInUser() initBuiltInUser()
initBuiltInApplication() initBuiltInApplication()
initBuiltInCert()
initBuiltInLdap() initBuiltInLdap()
} }
@ -90,6 +101,7 @@ func initBuiltInApplication() {
Logo: "https://cdn.casbin.com/logo/logo_1024x256.png", Logo: "https://cdn.casbin.com/logo/logo_1024x256.png",
HomepageUrl: "https://casdoor.org", HomepageUrl: "https://casdoor.org",
Organization: "built-in", Organization: "built-in",
Cert: "cert-built-in",
EnablePassword: true, EnablePassword: true,
EnableSignUp: true, EnableSignUp: true,
Providers: []*ProviderItem{}, Providers: []*ProviderItem{},
@ -109,6 +121,28 @@ func initBuiltInApplication() {
AddApplication(application) AddApplication(application)
} }
func initBuiltInCert() {
cert := getCert("admin", "cert-built-in")
if cert != nil {
return
}
cert = &Cert{
Owner: "admin",
Name: "cert-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Cert",
Scope: "JWT",
Type: "x509",
CryptoAlgorithm: "RSA",
BitSize: 4096,
ExpireInYears: 20,
PublicKey: tokenJwtPublicKey,
PrivateKey: tokenJwtPrivateKey,
}
AddCert(cert)
}
func initBuiltInLdap() { func initBuiltInLdap() {
ldap := GetLdap("ldap-built-in") ldap := GetLdap("ldap-built-in")
if ldap != nil { if ldap != nil {

View File

@ -72,13 +72,15 @@ func GetOidcDiscovery() OidcDiscovery {
return oidcDiscovery return oidcDiscovery
} }
func GetJSONWebKeySet() (jose.JSONWebKeySet, error) { func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
cert := GetDefaultCert()
//follows the protocol rfc 7517(draft) //follows the protocol rfc 7517(draft)
//link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html //link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
//or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key //or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
certPEMBlock := []byte(tokenJwtPublicKey) certPemBlock := []byte(cert.PublicKey)
certDERBlock, _ := pem.Decode(certPEMBlock) certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDERBlock.Bytes) x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
var jwk jose.JSONWebKey var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey jwk.Key = x509Cert.PublicKey

View File

@ -47,6 +47,8 @@ type Record struct {
RequestUri string `xorm:"varchar(1000)" json:"requestUri"` RequestUri string `xorm:"varchar(1000)" json:"requestUri"`
Action string `xorm:"varchar(1000)" json:"action"` Action string `xorm:"varchar(1000)" json:"action"`
ExtendedUser *User `xorm:"-" json:"extendedUser"`
IsTriggered bool `json:"isTriggered"` IsTriggered bool `json:"isTriggered"`
} }
@ -159,6 +161,11 @@ func SendWebhooks(record *Record) error {
} }
if matched { if matched {
if webhook.IsUserExtended {
user := getUser(record.Organization, record.User)
record.ExtendedUser = user
}
err := sendWebhook(webhook, record) err := sendWebhook(webhook, record)
if err != nil { if err != nil {
return err return err

126
object/role.go Normal file
View File

@ -0,0 +1,126 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casbin/casdoor/util"
"xorm.io/core"
)
type Role struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Users []string `xorm:"mediumtext" json:"users"`
Roles []string `xorm:"mediumtext" json:"roles"`
IsEnabled bool `json:"isEnabled"`
}
func GetRoleCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
count, err := session.Count(&Role{})
if err != nil {
panic(err)
}
return int(count)
}
func GetRoles(owner string) []*Role {
roles := []*Role{}
err := adapter.Engine.Desc("created_time").Find(&roles, &Role{Owner: owner})
if err != nil {
panic(err)
}
return roles
}
func GetPaginationRoles(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Role {
roles := []*Role{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&roles)
if err != nil {
panic(err)
}
return roles
}
func getRole(owner string, name string) *Role {
if owner == "" || name == "" {
return nil
}
role := Role{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&role)
if err != nil {
panic(err)
}
if existed {
return &role
} else {
return nil
}
}
func GetRole(id string) *Role {
owner, name := util.GetOwnerAndNameFromId(id)
return getRole(owner, name)
}
func UpdateRole(id string, role *Role) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getRole(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(role)
if err != nil {
panic(err)
}
return affected != 0
}
func AddRole(role *Role) bool {
affected, err := adapter.Engine.Insert(role)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteRole(role *Role) bool {
affected, err := adapter.Engine.ID(core.PK{role.Owner, role.Name}).Delete(&Role{})
if err != nil {
panic(err)
}
return affected != 0
}
func (role *Role) GetId() string {
return fmt.Sprintf("%s/%s", role.Owner, role.Name)
}

View File

@ -17,7 +17,6 @@ package object
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/casbin/casdoor/util" "github.com/casbin/casdoor/util"
"xorm.io/core" "xorm.io/core"
@ -46,11 +45,12 @@ type Token struct {
} }
type TokenWrapper struct { type TokenWrapper struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
IdToken string `json:"id_token"` IdToken string `json:"id_token"`
TokenType string `json:"token_type"` RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"` TokenType string `json:"token_type"`
Scope string `json:"scope"` ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
} }
func GetTokenCount(owner, field, value string) int { func GetTokenCount(owner, field, value string) int {
@ -190,6 +190,12 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
Code: "", Code: "",
} }
} }
if user.IsForbidden {
return &Code{
Message: "error: the user is forbidden to sign in, please contact the administrator",
Code: "",
}
}
msg, application := CheckOAuthLogin(clientId, responseType, redirectUri, scope, state) msg, application := CheckOAuthLogin(clientId, responseType, redirectUri, scope, state)
if msg != "" { if msg != "" {
@ -284,61 +290,76 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
} }
tokenWrapper := &TokenWrapper{ tokenWrapper := &TokenWrapper{
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
IdToken: token.AccessToken, IdToken: token.AccessToken,
TokenType: token.TokenType, RefreshToken: token.RefreshToken,
ExpiresIn: token.ExpiresIn, TokenType: token.TokenType,
Scope: token.Scope, ExpiresIn: token.ExpiresIn,
Scope: token.Scope,
} }
return tokenWrapper return tokenWrapper
} }
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string) *Code { func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string) *TokenWrapper {
// check parameters // check parameters
if grantType != "refresh_token" { if grantType != "refresh_token" {
return &Code{ return &TokenWrapper{
Message: "error: grant_type should be \"refresh_token\"", AccessToken: "error: grant_type should be \"refresh_token\"",
Code: "", TokenType: "",
ExpiresIn: 0,
Scope: "",
} }
} }
application := GetApplicationByClientId(clientId) application := GetApplicationByClientId(clientId)
if application == nil { if application == nil {
return &Code{ return &TokenWrapper{
Message: "error: invalid client_id", AccessToken: "error: invalid client_id",
Code: "", TokenType: "",
ExpiresIn: 0,
Scope: "",
} }
} }
if application.ClientSecret != clientSecret { if application.ClientSecret != clientSecret {
return &Code{ return &TokenWrapper{
Message: "error: invalid client_secret", AccessToken: "error: invalid client_secret",
Code: "", TokenType: "",
ExpiresIn: 0,
Scope: "",
} }
} }
// check whether the refresh token is valid, and has not expired. // check whether the refresh token is valid, and has not expired.
token := Token{RefreshToken: refreshToken} token := Token{RefreshToken: refreshToken}
existed, err := adapter.Engine.Get(&token) existed, err := adapter.Engine.Get(&token)
if err != nil || !existed { if err != nil || !existed {
return &Code{ return &TokenWrapper{
Message: "error: invalid refresh_token", AccessToken: "error: invalid refresh_token",
Code: "", TokenType: "",
ExpiresIn: 0,
Scope: "",
} }
} }
claims, err := ParseJwtToken(refreshToken)
cert := getCertByApplication(application)
_, err = ParseJwtToken(refreshToken, cert)
if err != nil { if err != nil {
return &Code{ return &TokenWrapper{
Message: "error: invalid refresh_token", AccessToken: fmt.Sprintf("error: %s", err.Error()),
Code: "", TokenType: "",
} ExpiresIn: 0,
} Scope: "",
if time.Now().Unix() > claims.ExpiresAt.Unix() {
return &Code{
Message: "error: expired refresh_token",
Code: "",
} }
} }
// generate a new token // generate a new token
user := getUser(application.Owner, token.User) user := getUser(application.Organization, token.User)
if user.IsForbidden {
return &TokenWrapper{
AccessToken: "error: the user is forbidden to sign in, please contact the administrator",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "") newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "")
if err != nil { if err != nil {
panic(err) panic(err)
@ -360,8 +381,14 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
} }
AddToken(newToken) AddToken(newToken)
return &Code{ tokenWrapper := &TokenWrapper{
Message: "", AccessToken: token.AccessToken,
Code: token.Code, IdToken: token.AccessToken,
RefreshToken: token.RefreshToken,
TokenType: token.TokenType,
ExpiresIn: token.ExpiresIn,
Scope: token.Scope,
} }
return tokenWrapper
} }

View File

@ -23,12 +23,6 @@ import (
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
//go:embed token_jwt_key.pem
var tokenJwtPublicKey string
//go:embed token_jwt_key.key
var tokenJwtPrivateKey string
type Claims struct { type Claims struct {
*User *User
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -68,9 +62,10 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
claims.ExpiresAt = jwt.NewNumericDate(refreshExpireTime) claims.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
refreshToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) refreshToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// Use "token_jwt_key.key" as RSA private key cert := getCertByApplication(application)
privateKey := tokenJwtPrivateKey
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey)) // RSA private key
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey))
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -84,14 +79,14 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
return tokenString, refreshTokenString, err return tokenString, refreshTokenString, err
} }
func ParseJwtToken(token string) (*Claims, error) { func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
// Use "token_jwt_key.pem" as RSA public key // RSA public key
publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(tokenJwtPublicKey)) publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.PublicKey))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -20,19 +20,14 @@ import (
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"fmt"
"math/big" "math/big"
"time" "time"
"github.com/casbin/casdoor/util"
) )
func generateRsaKeys(fileId string) { func generateRsaKeys(bitSize int, expireInYears int, commonName string, organization string) (string, string) {
// https://stackoverflow.com/questions/64104586/use-golang-to-get-rsa-key-the-same-way-openssl-genrsa // https://stackoverflow.com/questions/64104586/use-golang-to-get-rsa-key-the-same-way-openssl-genrsa
// https://stackoverflow.com/questions/43822945/golang-can-i-create-x509keypair-using-rsa-key // https://stackoverflow.com/questions/43822945/golang-can-i-create-x509keypair-using-rsa-key
bitSize := 4096
// Generate RSA key. // Generate RSA key.
key, err := rsa.GenerateKey(rand.Reader, bitSize) key, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil { if err != nil {
@ -50,12 +45,12 @@ func generateRsaKeys(fileId string) {
tml := x509.Certificate{ tml := x509.Certificate{
// you can add any attr that you need // you can add any attr that you need
NotBefore: time.Now(), NotBefore: time.Now(),
NotAfter: time.Now().AddDate(20, 0, 0), NotAfter: time.Now().AddDate(expireInYears, 0, 0),
// you have to generate a different serial number each execution // you have to generate a different serial number each execution
SerialNumber: big.NewInt(123456), SerialNumber: big.NewInt(123456),
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: "Casdoor Cert", CommonName: commonName,
Organization: []string{"Casdoor Organization"}, Organization: []string{organization},
}, },
BasicConstraintsValid: true, BasicConstraintsValid: true,
} }
@ -70,9 +65,5 @@ func generateRsaKeys(fileId string) {
Bytes: cert, Bytes: cert,
}) })
// Write private key to file. return string(certPem), string(privateKeyPem)
util.WriteBytesToPath(privateKeyPem, fmt.Sprintf("%s.key", fileId))
// Write certificate (aka public key) to file.
util.WriteBytesToPath(certPem, fmt.Sprintf("%s.pem", fileId))
} }

View File

@ -14,9 +14,20 @@
package object package object
import "testing" import (
"fmt"
"testing"
"github.com/casbin/casdoor/util"
)
func TestGenerateRsaKeys(t *testing.T) { func TestGenerateRsaKeys(t *testing.T) {
fileId := "token_jwt_key" fileId := "token_jwt_key"
generateRsaKeys(fileId) publicKey, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
// Write certificate (aka public key) to file.
util.WriteStringToPath(publicKey, fmt.Sprintf("%s.pem", fileId))
// Write private key to file.
util.WriteStringToPath(privateKey, fmt.Sprintf("%s.key", fileId))
} }

View File

@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/casbin/casdoor/util" "github.com/casbin/casdoor/util"
"xorm.io/core" "xorm.io/core"
@ -407,3 +408,7 @@ func LinkUserAccount(user *User, field string, value string) bool {
func (user *User) GetId() string { func (user *User) GetId() string {
return fmt.Sprintf("%s/%s", user.Owner, user.Name) return fmt.Sprintf("%s/%s", user.Owner, user.Name)
} }
func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/")
}

114
object/user_upload.go Normal file
View File

@ -0,0 +1,114 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casbin/casdoor/util"
"github.com/casbin/casdoor/xlsx"
)
func getUserMap(owner string) map[string]*User {
m := map[string]*User{}
users := GetUsers(owner)
for _, user := range users {
m[user.GetId()] = user
}
return m
}
func parseLineItem(line *[]string, i int) string {
if i >= len(*line) {
return ""
} else {
return (*line)[i]
}
}
func parseLineItemInt(line *[]string, i int) int {
s := parseLineItem(line, i)
return util.ParseInt(s)
}
func parseLineItemBool(line *[]string, i int) bool {
return parseLineItemInt(line, i) != 0
}
func UploadUsers(owner string, fileId string) bool {
table := xlsx.ReadXlsxFile(fileId)
oldUserMap := getUserMap(owner)
newUsers := []*User{}
for _, line := range table {
if parseLineItem(&line, 0) == "" {
continue
}
user := &User{
Owner: parseLineItem(&line, 0),
Name: parseLineItem(&line, 1),
CreatedTime: parseLineItem(&line, 2),
UpdatedTime: parseLineItem(&line, 3),
Id: parseLineItem(&line, 4),
Type: parseLineItem(&line, 5),
Password: parseLineItem(&line, 6),
PasswordSalt: parseLineItem(&line, 7),
DisplayName: parseLineItem(&line, 8),
Avatar: parseLineItem(&line, 9),
PermanentAvatar: "",
Email: parseLineItem(&line, 10),
Phone: parseLineItem(&line, 11),
Location: parseLineItem(&line, 12),
Address: []string{parseLineItem(&line, 13)},
Affiliation: parseLineItem(&line, 14),
Title: parseLineItem(&line, 15),
IdCardType: parseLineItem(&line, 16),
IdCard: parseLineItem(&line, 17),
Homepage: parseLineItem(&line, 18),
Bio: parseLineItem(&line, 19),
Tag: parseLineItem(&line, 20),
Region: parseLineItem(&line, 21),
Language: parseLineItem(&line, 22),
Gender: parseLineItem(&line, 23),
Birthday: parseLineItem(&line, 24),
Education: parseLineItem(&line, 25),
Score: parseLineItemInt(&line, 26),
Ranking: parseLineItemInt(&line, 27),
IsDefaultAvatar: false,
IsOnline: parseLineItemBool(&line, 28),
IsAdmin: parseLineItemBool(&line, 29),
IsGlobalAdmin: parseLineItemBool(&line, 30),
IsForbidden: parseLineItemBool(&line, 31),
IsDeleted: parseLineItemBool(&line, 32),
SignupApplication: parseLineItem(&line, 33),
Hash: "",
PreHash: "",
CreatedIp: parseLineItem(&line, 34),
LastSigninTime: parseLineItem(&line, 35),
LastSigninIp: parseLineItem(&line, 36),
Properties: map[string]string{},
}
if _, ok := oldUserMap[user.GetId()]; !ok {
newUsers = append(newUsers, user)
}
}
if len(newUsers) == 0 {
return false
}
return AddUsersInBatch(newUsers)
}

View File

@ -33,12 +33,13 @@ type Webhook struct {
Organization string `xorm:"varchar(100) index" json:"organization"` Organization string `xorm:"varchar(100) index" json:"organization"`
Url string `xorm:"varchar(100)" json:"url"` Url string `xorm:"varchar(100)" json:"url"`
Method string `xorm:"varchar(100)" json:"method"` Method string `xorm:"varchar(100)" json:"method"`
ContentType string `xorm:"varchar(100)" json:"contentType"` ContentType string `xorm:"varchar(100)" json:"contentType"`
Headers []*Header `xorm:"mediumtext" json:"headers"` Headers []*Header `xorm:"mediumtext" json:"headers"`
Events []string `xorm:"varchar(100)" json:"events"` Events []string `xorm:"varchar(100)" json:"events"`
IsEnabled bool `json:"isEnabled"` IsUserExtended bool `json:"isUserExtended"`
IsEnabled bool `json:"isEnabled"`
} }
func GetWebhookCount(owner, field, value string) int { func GetWebhookCount(owner, field, value string) int {

View File

@ -31,7 +31,8 @@ func AutoSigninFilter(ctx *context.Context) {
// "/page?access_token=123" // "/page?access_token=123"
accessToken := ctx.Input.Query("accessToken") accessToken := ctx.Input.Query("accessToken")
if accessToken != "" { if accessToken != "" {
claims, err := object.ParseJwtToken(accessToken) cert := object.GetDefaultCert()
claims, err := object.ParseJwtToken(accessToken, cert)
if err != nil { if err != nil {
responseError(ctx, "invalid JWT token") responseError(ctx, "invalid JWT token")
return return
@ -71,7 +72,8 @@ func AutoSigninFilter(ctx *context.Context) {
// Authorization: Bearer bearerToken // Authorization: Bearer bearerToken
bearerToken := parseBearerToken(ctx) bearerToken := parseBearerToken(ctx)
if bearerToken != "" { if bearerToken != "" {
claims, err := object.ParseJwtToken(bearerToken) cert := object.GetDefaultCert()
claims, err := object.ParseJwtToken(bearerToken, cert)
if err != nil { if err != nil {
responseError(ctx, err.Error()) responseError(ctx, err.Error())
return return

View File

@ -68,6 +68,13 @@ func initAPI() {
beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser") beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser")
beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser") beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser")
beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser") beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
beego.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
beego.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
beego.Router("/api/update-role", &controllers.ApiController{}, "POST:UpdateRole")
beego.Router("/api/add-role", &controllers.ApiController{}, "POST:AddRole")
beego.Router("/api/delete-role", &controllers.ApiController{}, "POST:DeleteRole")
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
@ -112,6 +119,7 @@ func initAPI() {
beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken") beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken")
beego.Router("/api/login/oauth/code", &controllers.ApiController{}, "POST:GetOAuthCode") beego.Router("/api/login/oauth/code", &controllers.ApiController{}, "POST:GetOAuthCode")
beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken") beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords") beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")
beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter") beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter")
@ -128,6 +136,12 @@ func initAPI() {
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer") beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer") beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer")
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
beego.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail") beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms") beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")

View File

@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
) )
@ -28,6 +29,24 @@ func FileExist(path string) bool {
return true return true
} }
func GetPath(path string) string {
return filepath.Dir(path)
}
func EnsureFileFolderExists(path string) {
p := GetPath(path)
if !FileExist(p) {
err := os.MkdirAll(p, os.ModePerm)
if err != nil {
panic(err)
}
}
}
func RemoveExt(filename string) string {
return filename[:len(filename)-len(filepath.Ext(filename))]
}
func UrlJoin(base string, path string) string { func UrlJoin(base string, path string) string {
res := fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(path, "/")) res := fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(path, "/"))
return res return res

21
util/setting.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import "fmt"
func GetUploadXlsxPath(fileId string) string {
return fmt.Sprintf("tmpFiles/%s.xlsx", fileId)
}

View File

@ -13,6 +13,7 @@
"codemirror": "^5.61.1", "codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"craco-less": "^1.17.1", "craco-less": "^1.17.1",
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0", "i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9", "i18next": "^19.8.9",
"moment": "^2.29.1", "moment": "^2.29.1",

View File

@ -23,12 +23,13 @@ import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage"; import OrganizationEditPage from "./OrganizationEditPage";
import UserListPage from "./UserListPage"; import UserListPage from "./UserListPage";
import UserEditPage from "./UserEditPage"; import UserEditPage from "./UserEditPage";
import RoleListPage from "./RoleListPage";
import RoleEditPage from "./RoleEditPage";
import ProviderListPage from "./ProviderListPage"; import ProviderListPage from "./ProviderListPage";
import ProviderEditPage from "./ProviderEditPage"; import ProviderEditPage from "./ProviderEditPage";
import ApplicationListPage from "./ApplicationListPage"; import ApplicationListPage from "./ApplicationListPage";
import ApplicationEditPage from "./ApplicationEditPage"; import ApplicationEditPage from "./ApplicationEditPage";
import ResourceListPage from "./ResourceListPage"; import ResourceListPage from "./ResourceListPage";
// import ResourceEditPage from "./ResourceEditPage";
import LdapEditPage from "./LdapEditPage"; import LdapEditPage from "./LdapEditPage";
import LdapSyncPage from "./LdapSyncPage"; import LdapSyncPage from "./LdapSyncPage";
import TokenListPage from "./TokenListPage"; import TokenListPage from "./TokenListPage";
@ -38,6 +39,8 @@ import WebhookListPage from "./WebhookListPage";
import WebhookEditPage from "./WebhookEditPage"; import WebhookEditPage from "./WebhookEditPage";
import SyncerListPage from "./SyncerListPage"; import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage"; import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import AccountPage from "./account/AccountPage"; import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage"; import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./CustomGithubCorner"; import CustomGithubCorner from "./CustomGithubCorner";
@ -101,6 +104,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/organizations' }); this.setState({ selectedMenuKey: '/organizations' });
} else if (uri.includes('/users')) { } else if (uri.includes('/users')) {
this.setState({ selectedMenuKey: '/users' }); this.setState({ selectedMenuKey: '/users' });
} else if (uri.includes('/roles')) {
this.setState({ selectedMenuKey: '/roles' });
} else if (uri.includes('/providers')) { } else if (uri.includes('/providers')) {
this.setState({ selectedMenuKey: '/providers' }); this.setState({ selectedMenuKey: '/providers' });
} else if (uri.includes('/applications')) { } else if (uri.includes('/applications')) {
@ -115,6 +120,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/webhooks' }); this.setState({ selectedMenuKey: '/webhooks' });
} else if (uri.includes('/syncers')) { } else if (uri.includes('/syncers')) {
this.setState({ selectedMenuKey: '/syncers' }); this.setState({ selectedMenuKey: '/syncers' });
} else if (uri.includes('/certs')) {
this.setState({ selectedMenuKey: '/certs' });
} else if (uri.includes('/signup')) { } else if (uri.includes('/signup')) {
this.setState({ selectedMenuKey: '/signup' }); this.setState({ selectedMenuKey: '/signup' });
} else if (uri.includes('/login')) { } else if (uri.includes('/login')) {
@ -324,6 +331,13 @@ class App extends Component {
</Link> </Link>
</Menu.Item> </Menu.Item>
); );
res.push(
<Menu.Item key="/roles">
<Link to="/roles">
{i18next.t("general:Roles")}
</Link>
</Menu.Item>
);
res.push( res.push(
<Menu.Item key="/providers"> <Menu.Item key="/providers">
<Link to="/providers"> <Link to="/providers">
@ -376,6 +390,13 @@ class App extends Component {
</Link> </Link>
</Menu.Item> </Menu.Item>
); );
res.push(
<Menu.Item key="/certs">
<Link to="/certs">
{i18next.t("general:Certs")}
</Link>
</Menu.Item>
);
res.push( res.push(
<Menu.Item key="/swagger"> <Menu.Item key="/swagger">
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}> <a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
@ -426,6 +447,8 @@ class App extends Component {
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)}/> <Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)}/>
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)}/> <Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)}/>
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />}/> <Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />}/>
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)}/>
<Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)}/> <Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)}/>
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)}/> <Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/> <Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>
@ -440,6 +463,8 @@ class App extends Component {
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)}/> <Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)}/> <Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)}/>
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)}/> <Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/> <Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />}/> <Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")} <Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}

View File

@ -21,7 +21,6 @@ import * as Setting from "./Setting";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import * as ProviderBackend from "./backend/ProviderBackend";
class ApplicationListPage extends BaseListPage { class ApplicationListPage extends BaseListPage {

254
web/src/CertEditPage.js Normal file
View File

@ -0,0 +1,254 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd';
import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import copy from "copy-to-clipboard";
import FileSaver from "file-saver";
const { Option } = Select;
const { TextArea } = Input;
class CertEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
certName: props.match.params.certName,
cert: null,
};
}
UNSAFE_componentWillMount() {
this.getCert();
}
getCert() {
CertBackend.getCert("admin", this.state.certName)
.then((cert) => {
this.setState({
cert: cert,
});
});
}
parseCertField(key, value) {
if (["port"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateCertField(key, value) {
value = this.parseCertField(key, value);
let cert = this.state.cert;
cert[key] = value;
this.setState({
cert: cert,
});
}
renderCert() {
return (
<Card size="small" title={
<div>
{i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.cert.name} onChange={e => {
this.updateCertField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.cert.displayName} onChange={e => {
this.updateCertField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Scope"), i18next.t("cert:Scope - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.scope} onChange={(value => {
this.updateCertField('scope', value);
})}>
{
[
{id: 'JWT', name: 'JWT'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.type} onChange={(value => {
this.updateCertField('type', value);
})}>
{
[
{id: 'x509', name: 'x509'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Crypto algorithm"), i18next.t("cert:Crypto algorithm - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.cryptoAlgorithm} onChange={(value => {
this.updateCertField('cryptoAlgorithm', value);
})}>
{
[
{id: 'RSA', name: 'RSA'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.bitSize} onChange={value => {
this.updateCertField('bitSize', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.expireInYears} onChange={value => {
this.updateCertField('expireInYears', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Public key"), i18next.t("cert:Public key - Tooltip"))} :
</Col>
<Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => {
copy(this.state.cert.publicKey);
Setting.showMessage("success", i18next.t("cert:Public key copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy public key")}
</Button>
<Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.publicKey], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.pem");
}}
>
{i18next.t("cert:Download public key")}
</Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.publicKey} onChange={e => {
this.updateCertField('publicKey', e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Private key"), i18next.t("cert:Private key - Tooltip"))} :
</Col>
<Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => {
copy(this.state.cert.privateKey);
Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy private key")}
</Button>
<Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.privateKey], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.key");
}}
>
{i18next.t("cert:Download private key")}
</Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.privateKey} onChange={e => {
this.updateCertField('privateKey', e.target.value);
}} />
</Col>
</Row>
</Card>
)
}
submitCertEdit(willExist) {
let cert = Setting.deepCopy(this.state.cert);
CertBackend.updateCert(this.state.cert.owner, this.state.certName, cert)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
certName: this.state.cert.name,
});
if (willExist) {
this.props.history.push(`/certs`);
} else {
this.props.history.push(`/certs/${this.state.cert.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateCertField('name', this.state.certName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
{
this.state.cert !== null ? this.renderCert() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
</div>
);
}
}
export default CertEditPage;

230
web/src/CertListPage.js Normal file
View File

@ -0,0 +1,230 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as CertBackend from "./backend/CertBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class CertListPage extends BaseListPage {
newCert() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.certname,
name: `cert_${randomName}`,
createdTime: moment().format(),
displayName: `New Cert - ${randomName}`,
scope: "JWT",
type: "x509",
cryptoAlgorithm: "RSA",
bitSize: 4096,
expireInYears: 20,
publicKey: "",
privateKey: "",
}
}
addCert() {
const newCert = this.newCert();
CertBackend.addCert(newCert)
.then((res) => {
Setting.showMessage("success", `Cert added successfully`);
this.props.history.push(`/certs/${newCert.name}`);
}
)
.catch(error => {
Setting.showMessage("error", `Cert failed to add: ${error}`);
});
}
deleteCert(i) {
CertBackend.deleteCert(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Cert deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`);
});
}
renderTable(certs) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '120px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/certs/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '180px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("cert:Scope"),
dataIndex: 'scope',
key: 'scope',
filterMultiple: false,
filters: [
{text: 'JWT', value: 'JWT'},
],
width: '110px',
sorter: true,
},
{
title: i18next.t("cert:Type"),
dataIndex: 'type',
key: 'type',
filterMultiple: false,
filters: [
{text: 'x509', value: 'x509'},
],
width: '110px',
sorter: true,
},
{
title: i18next.t("cert:Crypto algorithm"),
dataIndex: 'cryptoAlgorithm',
key: 'cryptoAlgorithm',
filterMultiple: false,
filters: [
{text: 'RSA', value: 'RSA'},
],
width: '190px',
sorter: true,
},
{
title: i18next.t("cert:Bit size"),
dataIndex: 'bitSize',
key: 'bitSize',
width: '130px',
sorter: true,
...this.getColumnSearchProps('bitSize'),
},
{
title: i18next.t("cert:Expire in years"),
dataIndex: 'expireInYears',
key: 'expireInYears',
width: '170px',
sorter: true,
...this.getColumnSearchProps('expireInYears'),
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/certs/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete cert: ${record.name} ?`}
onConfirm={() => this.deleteCert(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={certs} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addCert.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
CertBackend.getCerts("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default CertListPage;

View File

@ -119,14 +119,14 @@ class ProviderListPage extends BaseListPage {
{text: 'Storage', value: 'Storage'}, {text: 'Storage', value: 'Storage'},
{text: 'SAML', value: 'SAML'}, {text: 'SAML', value: 'SAML'},
], ],
width: '100px', width: '110px',
sorter: true, sorter: true,
}, },
{ {
title: i18next.t("provider:Type"), title: i18next.t("provider:Type"),
dataIndex: 'type', dataIndex: 'type',
key: 'type', key: 'type',
width: '80px', width: '110px',
align: 'center', align: 'center',
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
@ -152,13 +152,6 @@ class ProviderListPage extends BaseListPage {
return Setting.getShortText(text); return Setting.getShortText(text);
} }
}, },
// {
// title: 'Client secret',
// dataIndex: 'clientSecret',
// key: 'clientSecret',
// width: '150px',
// sorter: (a, b) => a.clientSecret.localeCompare(b.clientSecret),
// },
{ {
title: i18next.t("provider:Provider URL"), title: i18next.t("provider:Provider URL"),
dataIndex: 'providerUrl', dataIndex: 'providerUrl',

View File

@ -20,7 +20,6 @@ import * as RecordBackend from "./backend/RecordBackend";
import i18next from "i18next"; import i18next from "i18next";
import moment from "moment"; import moment from "moment";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import * as ProviderBackend from "./backend/ProviderBackend";
class RecordListPage extends BaseListPage { class RecordListPage extends BaseListPage {
@ -92,7 +91,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
dataIndex: 'organization', dataIndex: 'organization',
key: 'organization', key: 'organization',
width: '80px', width: '110px',
sorter: true, sorter: true,
...this.getColumnSearchProps('organization'), ...this.getColumnSearchProps('organization'),
render: (text, record, index) => { render: (text, record, index) => {
@ -122,7 +121,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Method"), title: i18next.t("general:Method"),
dataIndex: 'method', dataIndex: 'method',
key: 'method', key: 'method',
width: '100px', width: '110px',
sorter: true, sorter: true,
filterMultiple: false, filterMultiple: false,
filters: [ filters: [

View File

@ -21,7 +21,6 @@ import * as ResourceBackend from "./backend/ResourceBackend";
import i18next from "i18next"; import i18next from "i18next";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import * as ProviderBackend from "./backend/ProviderBackend";
class ResourceListPage extends BaseListPage { class ResourceListPage extends BaseListPage {
constructor(props) { constructor(props) {

219
web/src/RoleEditPage.js Normal file
View File

@ -0,0 +1,219 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
import * as RoleBackend from "./backend/RoleBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
const { Option } = Select;
class RoleEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
roleName: props.match.params.roleName,
role: null,
organizations: [],
users: [],
roles: [],
};
}
UNSAFE_componentWillMount() {
this.getRole();
this.getOrganizations();
}
getRole() {
RoleBackend.getRole(this.state.organizationName, this.state.roleName)
.then((role) => {
this.setState({
role: role,
});
this.getUsers(role.owner);
this.getRoles(role.owner);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
this.setState({
users: res,
});
});
}
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
this.setState({
roles: res,
});
});
}
parseRoleField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateRoleField(key, value) {
value = this.parseRoleField(key, value);
let role = this.state.role;
role[key] = value;
this.setState({
role: role,
});
}
renderRole() {
return (
<Card size="small" title={
<div>
{i18next.t("role:Edit Role")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitRoleEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitRoleEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.role.owner} onChange={(value => {this.updateRoleField('owner', value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.role.name} onChange={e => {
this.updateRoleField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.role.displayName} onChange={e => {
this.updateRoleField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.role.users} onChange={(value => {this.updateRoleField('users', value);})}>
{
this.state.users.map((user, index) => <Option key={index} value={`${user.owner}/${user.name}`}>{`${user.owner}/${user.name}`}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.role.roles} onChange={(value => {this.updateRoleField('roles', value);})}>
{
this.state.roles.filter(role => (role.owner !== this.state.role.owner || role.name !== this.state.role.name)).map((role, index) => <Option key={index} value={`${role.owner}/${role.name}`}>{`${role.owner}/${role.name}`}</Option>)
}
</Select>
</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"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.role.isEnabled} onChange={checked => {
this.updateRoleField('isEnabled', checked);
}} />
</Col>
</Row>
</Card>
)
}
submitRoleEdit(willExist) {
let role = Setting.deepCopy(this.state.role);
RoleBackend.updateRole(this.state.organizationName, this.state.roleName, role)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
roleName: this.state.role.name,
});
if (willExist) {
this.props.history.push(`/roles`);
} else {
this.props.history.push(`/roles/${this.state.role.owner}/${this.state.role.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateRoleField('name', this.state.roleName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
{
this.state.role !== null ? this.renderRole() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitRoleEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitRoleEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
</div>
);
}
}
export default RoleEditPage;

222
web/src/RoleListPage.js Normal file
View File

@ -0,0 +1,222 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as RoleBackend from "./backend/RoleBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class RoleListPage extends BaseListPage {
newRole() {
const randomName = Setting.getRandomName();
return {
owner: "built-in",
name: `role_${randomName}`,
createdTime: moment().format(),
displayName: `New Role - ${randomName}`,
users: [],
roles: [],
isEnabled: true,
}
}
addRole() {
const newRole = this.newRole();
RoleBackend.addRole(newRole)
.then((res) => {
Setting.showMessage("success", `Role added successfully`);
this.props.history.push(`/roles/${newRole.owner}/${newRole.name}`);
}
)
.catch(error => {
Setting.showMessage("error", `Role failed to add: ${error}`);
});
}
deleteRole(i) {
RoleBackend.deleteRole(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Role deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Role failed to delete: ${error}`);
});
}
renderTable(roles) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: 'owner',
key: 'owner',
width: '120px',
sorter: true,
...this.getColumnSearchProps('owner'),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '150px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/roles/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
width: '200px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("role:Sub users"),
dataIndex: 'users',
key: 'users',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('users'),
render: (text, record, index) => {
return Setting.getTags(text);
}
},
{
title: i18next.t("role:Sub roles"),
dataIndex: 'roles',
key: 'roles',
// width: '100px',
sorter: true,
...this.getColumnSearchProps('roles'),
render: (text, record, index) => {
return Setting.getTags(text);
}
},
{
title: i18next.t("general:Is enabled"),
dataIndex: 'isEnabled',
key: 'isEnabled',
width: '120px',
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
)
}
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/roles/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete role: ${record.name} ?`}
onConfirm={() => this.deleteRole(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={roles} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Roles")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addRole.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
RoleBackend.getRoles("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default RoleListPage;

View File

@ -590,7 +590,7 @@ export function getNewRowNameForTable(table, rowName) {
} }
export function getTagColor(s) { export function getTagColor(s) {
return "success"; return "processing";
} }
export function getTags(tags) { export function getTags(tags) {

View File

@ -74,6 +74,7 @@ class SignupTable extends React.Component {
{id: 'Display name', name: 'Display name'}, {id: 'Display name', name: 'Display name'},
{id: 'Affiliation', name: 'Affiliation'}, {id: 'Affiliation', name: 'Affiliation'},
{id: 'Country/Region', name: 'Country/Region'}, {id: 'Country/Region', name: 'Country/Region'},
{id: 'ID card', name: 'ID card'},
{id: 'Email', name: 'Email'}, {id: 'Email', name: 'Email'},
{id: 'Password', name: 'Password'}, {id: 'Password', name: 'Password'},
{id: 'Confirm password', name: 'Confirm password'}, {id: 'Confirm password', name: 'Confirm password'},

View File

@ -20,7 +20,6 @@ import * as Setting from "./Setting";
import * as SyncerBackend from "./backend/SyncerBackend"; import * as SyncerBackend from "./backend/SyncerBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import * as ProviderBackend from "./backend/ProviderBackend";
class SyncerListPage extends BaseListPage { class SyncerListPage extends BaseListPage {
@ -112,7 +111,7 @@ class SyncerListPage extends BaseListPage {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: 'createdTime',
key: 'createdTime', key: 'createdTime',
width: '180px', width: '160px',
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -187,7 +186,7 @@ class SyncerListPage extends BaseListPage {
title: i18next.t("syncer:Sync interval"), title: i18next.t("syncer:Sync interval"),
dataIndex: 'syncInterval', dataIndex: 'syncInterval',
key: 'syncInterval', key: 'syncInterval',
width: '120px', width: '130px',
sorter: true, sorter: true,
...this.getColumnSearchProps('syncInterval'), ...this.getColumnSearchProps('syncInterval'),
}, },

View File

@ -176,7 +176,7 @@ class TokenListPage extends BaseListPage {
title: i18next.t("token:Scope"), title: i18next.t("token:Scope"),
dataIndex: 'scope', dataIndex: 'scope',
key: 'scope', key: 'scope',
width: '100px', width: '110px',
sorter: true, sorter: true,
...this.getColumnSearchProps('scope'), ...this.getColumnSearchProps('scope'),
}, },

View File

@ -25,11 +25,11 @@ import PasswordModal from "./PasswordModal";
import ResetModal from "./ResetModal"; import ResetModal from "./ResetModal";
import AffiliationSelect from "./common/AffiliationSelect"; import AffiliationSelect from "./common/AffiliationSelect";
import OAuthWidget from "./common/OAuthWidget"; import OAuthWidget from "./common/OAuthWidget";
import SamlWidget from "./common/SamlWidget";
import SelectRegionBox from "./SelectRegionBox"; import SelectRegionBox from "./SelectRegionBox";
import {Controlled as CodeMirror} from 'react-codemirror2'; import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
import SamlWidget from "./common/SamlWidget";
require('codemirror/theme/material-darker.css'); require('codemirror/theme/material-darker.css');
require("codemirror/mode/javascript/javascript"); require("codemirror/mode/javascript/javascript");

View File

@ -14,13 +14,13 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd'; import {Button, Popconfirm, Switch, Table, Upload} from 'antd';
import {UploadOutlined} from "@ant-design/icons";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import * as ProviderBackend from "./backend/ProviderBackend";
class UserListPage extends BaseListPage { class UserListPage extends BaseListPage {
constructor(props) { constructor(props) {
@ -93,6 +93,43 @@ class UserListPage extends BaseListPage {
}); });
} }
uploadFile(info) {
const { status, response: res } = info.file;
if (status === 'done') {
if (res.status === 'ok') {
Setting.showMessage("success", `Users uploaded successfully, refreshing the page`);
const { pagination } = this.state;
this.fetch({ pagination });
} else {
Setting.showMessage("error", `Users failed to upload: ${res.msg}`);
}
} else if (status === 'error') {
Setting.showMessage("error", `File failed to upload`);
}
}
renderUpload() {
const props = {
name: 'file',
accept: '.xlsx',
method: 'post',
action: `${Setting.ServerUrl}/api/upload-users`,
withCredentials: true,
onChange: (info) => {
this.uploadFile(info);
},
};
return (
<Upload {...props}>
<Button type="primary" size="small">
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
</Button>
</Upload>
)
}
renderTable(users) { renderTable(users) {
// transfer country code to name based on selected language // transfer country code to name based on selected language
var countries = require("i18n-iso-countries"); var countries = require("i18n-iso-countries");
@ -138,7 +175,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: (Setting.isMobile()) ? "80px" : "100px", width: (Setting.isMobile()) ? "80px" : "110px",
fixed: 'left', fixed: 'left',
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps('name'),
@ -164,7 +201,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: 'displayName', dataIndex: 'displayName',
key: 'displayName', key: 'displayName',
width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('displayName'), ...this.getColumnSearchProps('displayName'),
}, },
@ -215,7 +252,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("user:Affiliation"), title: i18next.t("user:Affiliation"),
dataIndex: 'affiliation', dataIndex: 'affiliation',
key: 'affiliation', key: 'affiliation',
width: '120px', width: '140px',
sorter: true, sorter: true,
...this.getColumnSearchProps('affiliation'), ...this.getColumnSearchProps('affiliation'),
}, },
@ -223,7 +260,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("user:Country/Region"), title: i18next.t("user:Country/Region"),
dataIndex: 'region', dataIndex: 'region',
key: 'region', key: 'region',
width: '120px', width: '140px',
sorter: true, sorter: true,
...this.getColumnSearchProps('region'), ...this.getColumnSearchProps('region'),
}, },
@ -231,7 +268,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("user:Tag"), title: i18next.t("user:Tag"),
dataIndex: 'tag', dataIndex: 'tag',
key: 'tag', key: 'tag',
width: '100px', width: '110px',
sorter: true, sorter: true,
...this.getColumnSearchProps('tag'), ...this.getColumnSearchProps('tag'),
}, },
@ -251,7 +288,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("user:Is global admin"), title: i18next.t("user:Is global admin"),
dataIndex: 'isGlobalAdmin', dataIndex: 'isGlobalAdmin',
key: 'isGlobalAdmin', key: 'isGlobalAdmin',
width: '110px', width: '140px',
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
@ -317,8 +354,11 @@ class UserListPage extends BaseListPage {
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={users} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: 'max-content'}} columns={columns} dataSource={users} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Users")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Users")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addUser.bind(this)}>{i18next.t("general:Add")}</Button> <Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addUser.bind(this)}>{i18next.t("general:Add")}</Button>
{
this.renderUpload()
}
</div> </div>
)} )}
loading={this.state.loading} loading={this.state.loading}

View File

@ -21,8 +21,73 @@ import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import WebhookHeaderTable from "./WebhookHeaderTable"; import WebhookHeaderTable from "./WebhookHeaderTable";
import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css";
require('codemirror/theme/material-darker.css');
require("codemirror/mode/javascript/javascript");
const { Option } = Select; const { Option } = Select;
const previewTemplate = {
"id": 9078,
"owner": "built-in",
"name": "68f55b28-7380-46b1-9bde-64fe1576e3b3",
"createdTime": "2022-01-01T01:03:42+08:00",
"organization": "built-in",
"clientIp": "159.89.126.192",
"user": "admin",
"method": "POST",
"requestUri": "/api/login",
"action": "login",
"isTriggered": false,
};
const userTemplate = {
"owner": "built-in",
"name": "admin",
"createdTime": "2020-07-16T21:46:52+08:00",
"updatedTime": "",
"id": "9eb20f79-3bb5-4e74-99ac-39e3b9a171e8",
"type": "normal-user",
"password": "123",
"passwordSalt": "",
"displayName": "Admin",
"avatar": "https://cdn.casbin.com/usercontent/admin/avatar/1596241359.png",
"permanentAvatar": "https://cdn.casbin.com/casdoor/avatar/casbin/admin.png",
"email": "admin@example.com",
"phone": "",
"location": "",
"address": null,
"affiliation": "",
"title": "",
"score": 10000,
"ranking": 10,
"isOnline": false,
"isAdmin": true,
"isGlobalAdmin": false,
"isForbidden": false,
"isDeleted": false,
"signupApplication": "app-casnode",
"properties": {
"bio": "",
"checkinDate": "20200801",
"editorType": "",
"emailVerifiedTime": "2020-07-16T21:46:52+08:00",
"fileQuota": "50",
"location": "",
"no": "22",
"oauth_QQ_displayName": "",
"oauth_QQ_verifiedTime": "",
"oauth_WeChat_displayName": "",
"oauth_WeChat_verifiedTime": "",
"onlineStatus": "false",
"phoneVerifiedTime": "",
"renameQuota": "3",
"tagline": "",
"website": ""
}
};
class WebhookEditPage extends React.Component { class WebhookEditPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -75,6 +140,12 @@ class WebhookEditPage extends React.Component {
} }
renderWebhook() { renderWebhook() {
let preview = Setting.deepCopy(previewTemplate);
if (this.state.webhook.isUserExtended) {
preview["extendedUser"] = userTemplate;
}
const previewText = JSON.stringify(preview, null, 2);
return ( return (
<Card size="small" title={ <Card size="small" title={
<div> <div>
@ -181,6 +252,30 @@ class WebhookEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("webhook:Is user extended"), i18next.t("webhook:Is user extended - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.webhook.isUserExtended} onChange={checked => {
this.updateWebhookField('isUserExtended', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
<Col span={22} >
<div style={{width: "900px", height: "300px"}} >
<CodeMirror
value={previewText}
options={{mode: 'javascript', theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {}}
/>
</div>
</Col>
</Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :

View File

@ -72,7 +72,7 @@ class WebhookListPage extends BaseListPage {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
dataIndex: 'organization', dataIndex: 'organization',
key: 'organization', key: 'organization',
width: '80px', width: '110px',
sorter: true, sorter: true,
...this.getColumnSearchProps('organization'), ...this.getColumnSearchProps('organization'),
render: (text, record, index) => { render: (text, record, index) => {
@ -157,6 +157,18 @@ class WebhookListPage extends BaseListPage {
return Setting.getTags(text); return Setting.getTags(text);
} }
}, },
{
title: i18next.t("webhook:Is user extended"),
dataIndex: 'isUserExtended',
key: 'isUserExtended',
width: '160px',
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
)
}
},
{ {
title: i18next.t("general:Is enabled"), title: i18next.t("general:Is enabled"),
dataIndex: 'isEnabled', dataIndex: 'isEnabled',

View File

@ -448,13 +448,17 @@ class LoginPage extends React.Component {
return ( return (
<React.Fragment> <React.Fragment>
<span style={{float: "left"}}> <span style={{float: "left"}}>
<a onClick={() => { {
this.setState({ !application.enableCodeSignin ? null : (
isCodeSignin: !this.state.isCodeSignin, <a onClick={() => {
}); this.setState({
}}> isCodeSignin: !this.state.isCodeSignin,
{this.state.isCodeSignin ? i18next.t("login:Sign in with password") : i18next.t("login:Sign in with code")} });
</a> }}>
{this.state.isCodeSignin ? i18next.t("login:Sign in with password") : i18next.t("login:Sign in with code")}
</a>
)
}
</span> </span>
<span style={{float: "right"}}> <span style={{float: "right"}}>
{i18next.t("login:No account?")}&nbsp; {i18next.t("login:No account?")}&nbsp;

View File

@ -225,6 +225,28 @@ class SignupPage extends React.Component {
<Input /> <Input />
</Form.Item> </Form.Item>
) )
} else if (signupItem.name === "ID card") {
return (
<Form.Item
name="idCard"
key="idCard"
label={i18next.t("user:ID card")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your ID card number!"),
whitespace: true,
},
{
required: required,
pattern: new RegExp(/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9X]$/, "g"),
message: i18next.t("signup:Please input the correct ID card number!"),
},
]}
>
<Input />
</Form.Item>
)
} else if (signupItem.name === "Country/Region") { } else if (signupItem.name === "Country/Region") {
return ( return (
<Form.Item <Form.Item

View File

@ -0,0 +1,56 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getCerts(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-certs?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getCert(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-cert?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateCert(owner, name, cert) {
let newCert = Setting.deepCopy(cert);
return fetch(`${Setting.ServerUrl}/api/update-cert?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newCert),
}).then(res => res.json());
}
export function addCert(cert) {
let newCert = Setting.deepCopy(cert);
return fetch(`${Setting.ServerUrl}/api/add-cert`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newCert),
}).then(res => res.json());
}
export function deleteCert(cert) {
let newCert = Setting.deepCopy(cert);
return fetch(`${Setting.ServerUrl}/api/delete-cert`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newCert),
}).then(res => res.json());
}

View File

@ -0,0 +1,56 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getRoles(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-roles?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getRole(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-role?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateRole(owner, name, role) {
let newRole = Setting.deepCopy(role);
return fetch(`${Setting.ServerUrl}/api/update-role?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newRole),
}).then(res => res.json());
}
export function addRole(role) {
let newRole = Setting.deepCopy(role);
return fetch(`${Setting.ServerUrl}/api/add-role`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newRole),
}).then(res => res.json());
}
export function deleteRole(role) {
let newRole = Setting.deepCopy(role);
return fetch(`${Setting.ServerUrl}/api/delete-role`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newRole),
}).then(res => res.json());
}

View File

@ -33,6 +33,29 @@
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip"
}, },
"cert": {
"Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key",
"Copy public key": "Copy public key",
"Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key",
"Download public key": "Download public key",
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
},
"code": { "code": {
"Code You Received": "Code You Received", "Code You Received": "Code You Received",
"Email code": "Email code", "Email code": "Email code",
@ -69,6 +92,7 @@
"Avatar - Tooltip": "Avatar to show to others", "Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Back Home", "Back Home": "Back Home",
"Captcha": "Captcha", "Captcha": "Captcha",
"Certs": "Certs",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -122,6 +146,7 @@
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
"Resources": "Resources", "Resources": "Resources",
"Roles": "Roles",
"Save": "Save", "Save": "Save",
"Save & Exit": "Save & Exit", "Save & Exit": "Save & Exit",
"Signin URL": "Signin URL", "Signin URL": "Signin URL",
@ -301,6 +326,13 @@
"Upload a file...": "Upload a file...", "Upload a file...": "Upload a file...",
"User": "User" "User": "User"
}, },
"role": {
"Edit Role": "Edit Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
"Sub users - Tooltip": "Sub users - Tooltip"
},
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Confirm": "Confirm", "Confirm": "Confirm",
@ -309,7 +341,9 @@
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
"Please confirm your password!": "Please confirm your password!", "Please confirm your password!": "Please confirm your password!",
"Please input the correct ID card number!": "Please input the correct ID card number!",
"Please input your Email!": "Please input your Email!", "Please input your Email!": "Please input your Email!",
"Please input your ID card number!": "Please input your ID card number!",
"Please input your address!": "Please input your address!", "Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!", "Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!", "Please input your display name!": "Please input your display name!",
@ -375,6 +409,7 @@
"Empty input!": "Empty input!", "Empty input!": "Empty input!",
"Homepage": "Homepage", "Homepage": "Homepage",
"Homepage - Tooltip": "Homepage - Tooltip", "Homepage - Tooltip": "Homepage - Tooltip",
"ID card": "ID card",
"Input your email": "Input your email", "Input your email": "Input your email",
"Input your phone number": "Input your phone number", "Input your phone number": "Input your phone number",
"Is admin": "Is admin", "Is admin": "Is admin",
@ -410,6 +445,7 @@
"Title - Tooltip": "Title - Tooltip", "Title - Tooltip": "Title - Tooltip",
"Two passwords you typed do not match.": "Two passwords you typed do not match.", "Two passwords you typed do not match.": "Two passwords you typed do not match.",
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"input password": "input password" "input password": "input password"
}, },
@ -421,6 +457,8 @@
"Events - Tooltip": "Events - Tooltip", "Events - Tooltip": "Events - Tooltip",
"Headers": "Headers", "Headers": "Headers",
"Headers - Tooltip": "Headers - Tooltip", "Headers - Tooltip": "Headers - Tooltip",
"Is user extended": "Is user extended",
"Is user extended - Tooltip": "Is user extended - Tooltip",
"Method": "Method", "Method": "Method",
"Method - Tooltip": "Method - Tooltip", "Method - Tooltip": "Method - Tooltip",
"Name": "Name", "Name": "Name",

View File

@ -33,6 +33,29 @@
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip"
}, },
"cert": {
"Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key",
"Copy public key": "Copy public key",
"Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key",
"Download public key": "Download public key",
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
},
"code": { "code": {
"Code You Received": "Code You Received", "Code You Received": "Code You Received",
"Email code": "Email code", "Email code": "Email code",
@ -69,6 +92,7 @@
"Avatar - Tooltip": "Avatar - Tooltip", "Avatar - Tooltip": "Avatar - Tooltip",
"Back Home": "Back Home", "Back Home": "Back Home",
"Captcha": "Captcha", "Captcha": "Captcha",
"Certs": "Certs",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -122,6 +146,7 @@
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
"Resources": "Resources", "Resources": "Resources",
"Roles": "Roles",
"Save": "Save", "Save": "Save",
"Save & Exit": "Save & Exit", "Save & Exit": "Save & Exit",
"Signin URL": "Signin URL", "Signin URL": "Signin URL",
@ -301,6 +326,13 @@
"Upload a file...": "Upload a file...", "Upload a file...": "Upload a file...",
"User": "User" "User": "User"
}, },
"role": {
"Edit Role": "Edit Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
"Sub users - Tooltip": "Sub users - Tooltip"
},
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Confirm": "Confirm", "Confirm": "Confirm",
@ -309,7 +341,9 @@
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
"Please confirm your password!": "Please confirm your password!", "Please confirm your password!": "Please confirm your password!",
"Please input the correct ID card number!": "Please input the correct ID card number!",
"Please input your Email!": "Please input your Email!", "Please input your Email!": "Please input your Email!",
"Please input your ID card number!": "Please input your ID card number!",
"Please input your address!": "Please input your address!", "Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!", "Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!", "Please input your display name!": "Please input your display name!",
@ -375,6 +409,7 @@
"Empty input!": "Empty input!", "Empty input!": "Empty input!",
"Homepage": "Homepage", "Homepage": "Homepage",
"Homepage - Tooltip": "Homepage - Tooltip", "Homepage - Tooltip": "Homepage - Tooltip",
"ID card": "ID card",
"Input your email": "Input your email", "Input your email": "Input your email",
"Input your phone number": "Input your phone number", "Input your phone number": "Input your phone number",
"Is admin": "Is admin", "Is admin": "Is admin",
@ -410,6 +445,7 @@
"Title - Tooltip": "Title - Tooltip", "Title - Tooltip": "Title - Tooltip",
"Two passwords you typed do not match.": "Two passwords you typed do not match.", "Two passwords you typed do not match.": "Two passwords you typed do not match.",
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"input password": "input password" "input password": "input password"
}, },
@ -421,6 +457,8 @@
"Events - Tooltip": "Events - Tooltip", "Events - Tooltip": "Events - Tooltip",
"Headers": "Headers", "Headers": "Headers",
"Headers - Tooltip": "Headers - Tooltip", "Headers - Tooltip": "Headers - Tooltip",
"Is user extended": "Is user extended",
"Is user extended - Tooltip": "Is user extended - Tooltip",
"Method": "Method", "Method": "Method",
"Method - Tooltip": "Method - Tooltip", "Method - Tooltip": "Method - Tooltip",
"Name": "Name", "Name": "Name",

View File

@ -33,6 +33,29 @@
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip"
}, },
"cert": {
"Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key",
"Copy public key": "Copy public key",
"Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key",
"Download public key": "Download public key",
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
},
"code": { "code": {
"Code You Received": "Code You Received", "Code You Received": "Code You Received",
"Email code": "Email code", "Email code": "Email code",
@ -69,6 +92,7 @@
"Avatar - Tooltip": "Avatar to show to others", "Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Back Home", "Back Home": "Back Home",
"Captcha": "Captcha", "Captcha": "Captcha",
"Certs": "Certs",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -122,6 +146,7 @@
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
"Resources": "Resources", "Resources": "Resources",
"Roles": "Roles",
"Save": "Save", "Save": "Save",
"Save & Exit": "Save & Exit", "Save & Exit": "Save & Exit",
"Signin URL": "Signin URL", "Signin URL": "Signin URL",
@ -301,6 +326,13 @@
"Upload a file...": "Upload a file...", "Upload a file...": "Upload a file...",
"User": "User" "User": "User"
}, },
"role": {
"Edit Role": "Edit Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
"Sub users - Tooltip": "Sub users - Tooltip"
},
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Confirm": "Confirm", "Confirm": "Confirm",
@ -309,7 +341,9 @@
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
"Please confirm your password!": "Please confirm your password!", "Please confirm your password!": "Please confirm your password!",
"Please input the correct ID card number!": "Please input the correct ID card number!",
"Please input your Email!": "Please input your Email!", "Please input your Email!": "Please input your Email!",
"Please input your ID card number!": "Please input your ID card number!",
"Please input your address!": "Please input your address!", "Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!", "Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!", "Please input your display name!": "Please input your display name!",
@ -375,6 +409,7 @@
"Empty input!": "Empty input!", "Empty input!": "Empty input!",
"Homepage": "Homepage", "Homepage": "Homepage",
"Homepage - Tooltip": "Homepage - Tooltip", "Homepage - Tooltip": "Homepage - Tooltip",
"ID card": "ID card",
"Input your email": "Input your email", "Input your email": "Input your email",
"Input your phone number": "Input your phone number", "Input your phone number": "Input your phone number",
"Is admin": "Is admin", "Is admin": "Is admin",
@ -410,6 +445,7 @@
"Title - Tooltip": "Title - Tooltip", "Title - Tooltip": "Title - Tooltip",
"Two passwords you typed do not match.": "Two passwords you typed do not match.", "Two passwords you typed do not match.": "Two passwords you typed do not match.",
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"input password": "input password" "input password": "input password"
}, },
@ -421,6 +457,8 @@
"Events - Tooltip": "Events - Tooltip", "Events - Tooltip": "Events - Tooltip",
"Headers": "Headers", "Headers": "Headers",
"Headers - Tooltip": "Headers - Tooltip", "Headers - Tooltip": "Headers - Tooltip",
"Is user extended": "Is user extended",
"Is user extended - Tooltip": "Is user extended - Tooltip",
"Method": "Method", "Method": "Method",
"Method - Tooltip": "Method - Tooltip", "Method - Tooltip": "Method - Tooltip",
"Name": "Name", "Name": "Name",

View File

@ -33,6 +33,29 @@
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip"
}, },
"cert": {
"Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key",
"Copy public key": "Copy public key",
"Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key",
"Download public key": "Download public key",
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
},
"code": { "code": {
"Code You Received": "Code You Received", "Code You Received": "Code You Received",
"Email code": "Email code", "Email code": "Email code",
@ -69,6 +92,7 @@
"Avatar - Tooltip": "Avatar to show to others", "Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Back Home", "Back Home": "Back Home",
"Captcha": "Captcha", "Captcha": "Captcha",
"Certs": "Certs",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -122,6 +146,7 @@
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
"Resources": "Resources", "Resources": "Resources",
"Roles": "Roles",
"Save": "Save", "Save": "Save",
"Save & Exit": "Save & Exit", "Save & Exit": "Save & Exit",
"Signin URL": "Signin URL", "Signin URL": "Signin URL",
@ -301,6 +326,13 @@
"Upload a file...": "Upload a file...", "Upload a file...": "Upload a file...",
"User": "User" "User": "User"
}, },
"role": {
"Edit Role": "Edit Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
"Sub users - Tooltip": "Sub users - Tooltip"
},
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Confirm": "Confirm", "Confirm": "Confirm",
@ -309,7 +341,9 @@
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
"Please confirm your password!": "Please confirm your password!", "Please confirm your password!": "Please confirm your password!",
"Please input the correct ID card number!": "Please input the correct ID card number!",
"Please input your Email!": "Please input your Email!", "Please input your Email!": "Please input your Email!",
"Please input your ID card number!": "Please input your ID card number!",
"Please input your address!": "Please input your address!", "Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!", "Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!", "Please input your display name!": "Please input your display name!",
@ -375,6 +409,7 @@
"Empty input!": "Empty input!", "Empty input!": "Empty input!",
"Homepage": "Homepage", "Homepage": "Homepage",
"Homepage - Tooltip": "Homepage - Tooltip", "Homepage - Tooltip": "Homepage - Tooltip",
"ID card": "ID card",
"Input your email": "Input your email", "Input your email": "Input your email",
"Input your phone number": "Input your phone number", "Input your phone number": "Input your phone number",
"Is admin": "Is admin", "Is admin": "Is admin",
@ -410,6 +445,7 @@
"Title - Tooltip": "Title - Tooltip", "Title - Tooltip": "Title - Tooltip",
"Two passwords you typed do not match.": "Two passwords you typed do not match.", "Two passwords you typed do not match.": "Two passwords you typed do not match.",
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"input password": "input password" "input password": "input password"
}, },
@ -421,6 +457,8 @@
"Events - Tooltip": "Events - Tooltip", "Events - Tooltip": "Events - Tooltip",
"Headers": "Headers", "Headers": "Headers",
"Headers - Tooltip": "Headers - Tooltip", "Headers - Tooltip": "Headers - Tooltip",
"Is user extended": "Is user extended",
"Is user extended - Tooltip": "Is user extended - Tooltip",
"Method": "Method", "Method": "Method",
"Method - Tooltip": "Method - Tooltip", "Method - Tooltip": "Method - Tooltip",
"Name": "Name", "Name": "Name",

View File

@ -33,6 +33,29 @@
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip"
}, },
"cert": {
"Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key",
"Copy public key": "Copy public key",
"Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key",
"Download public key": "Download public key",
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
},
"code": { "code": {
"Code You Received": "Code You Received", "Code You Received": "Code You Received",
"Email code": "Email code", "Email code": "Email code",
@ -69,6 +92,7 @@
"Avatar - Tooltip": "Avatar to show to others", "Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Back Home", "Back Home": "Back Home",
"Captcha": "Captcha", "Captcha": "Captcha",
"Certs": "Certs",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -122,6 +146,7 @@
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
"Resources": "Resources", "Resources": "Resources",
"Roles": "Roles",
"Save": "Save", "Save": "Save",
"Save & Exit": "Save & Exit", "Save & Exit": "Save & Exit",
"Signin URL": "Signin URL", "Signin URL": "Signin URL",
@ -301,6 +326,13 @@
"Upload a file...": "Upload a file...", "Upload a file...": "Upload a file...",
"User": "User" "User": "User"
}, },
"role": {
"Edit Role": "Edit Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
"Sub users - Tooltip": "Sub users - Tooltip"
},
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Confirm": "Confirm", "Confirm": "Confirm",
@ -309,7 +341,9 @@
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
"Please confirm your password!": "Please confirm your password!", "Please confirm your password!": "Please confirm your password!",
"Please input the correct ID card number!": "Please input the correct ID card number!",
"Please input your Email!": "Please input your Email!", "Please input your Email!": "Please input your Email!",
"Please input your ID card number!": "Please input your ID card number!",
"Please input your address!": "Please input your address!", "Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!", "Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!", "Please input your display name!": "Please input your display name!",
@ -375,6 +409,7 @@
"Empty input!": "Empty input!", "Empty input!": "Empty input!",
"Homepage": "Homepage", "Homepage": "Homepage",
"Homepage - Tooltip": "Homepage - Tooltip", "Homepage - Tooltip": "Homepage - Tooltip",
"ID card": "ID card",
"Input your email": "Input your email", "Input your email": "Input your email",
"Input your phone number": "Input your phone number", "Input your phone number": "Input your phone number",
"Is admin": "Is admin", "Is admin": "Is admin",
@ -410,6 +445,7 @@
"Title - Tooltip": "Title - Tooltip", "Title - Tooltip": "Title - Tooltip",
"Two passwords you typed do not match.": "Two passwords you typed do not match.", "Two passwords you typed do not match.": "Two passwords you typed do not match.",
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"input password": "input password" "input password": "input password"
}, },
@ -421,6 +457,8 @@
"Events - Tooltip": "Events - Tooltip", "Events - Tooltip": "Events - Tooltip",
"Headers": "Headers", "Headers": "Headers",
"Headers - Tooltip": "Headers - Tooltip", "Headers - Tooltip": "Headers - Tooltip",
"Is user extended": "Is user extended",
"Is user extended - Tooltip": "Is user extended - Tooltip",
"Method": "Method", "Method": "Method",
"Method - Tooltip": "Method - Tooltip", "Method - Tooltip": "Method - Tooltip",
"Name": "Name", "Name": "Name",

View File

@ -33,6 +33,29 @@
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip"
}, },
"cert": {
"Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key",
"Copy public key": "Copy public key",
"Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key",
"Download public key": "Download public key",
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
},
"code": { "code": {
"Code You Received": "Code You Received", "Code You Received": "Code You Received",
"Email code": "Email code", "Email code": "Email code",
@ -69,6 +92,7 @@
"Avatar - Tooltip": "Avatar to show to others", "Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Back Home", "Back Home": "Back Home",
"Captcha": "Captcha", "Captcha": "Captcha",
"Certs": "Certs",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -122,6 +146,7 @@
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
"Resources": "Resources", "Resources": "Resources",
"Roles": "Roles",
"Save": "Save", "Save": "Save",
"Save & Exit": "Save & Exit", "Save & Exit": "Save & Exit",
"Signin URL": "Signin URL", "Signin URL": "Signin URL",
@ -301,6 +326,13 @@
"Upload a file...": "Upload a file...", "Upload a file...": "Upload a file...",
"User": "User" "User": "User"
}, },
"role": {
"Edit Role": "Edit Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
"Sub users - Tooltip": "Sub users - Tooltip"
},
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Confirm": "Confirm", "Confirm": "Confirm",
@ -309,7 +341,9 @@
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
"Please confirm your password!": "Please confirm your password!", "Please confirm your password!": "Please confirm your password!",
"Please input the correct ID card number!": "Please input the correct ID card number!",
"Please input your Email!": "Please input your Email!", "Please input your Email!": "Please input your Email!",
"Please input your ID card number!": "Please input your ID card number!",
"Please input your address!": "Please input your address!", "Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!", "Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!", "Please input your display name!": "Please input your display name!",
@ -375,6 +409,7 @@
"Empty input!": "Empty input!", "Empty input!": "Empty input!",
"Homepage": "Homepage", "Homepage": "Homepage",
"Homepage - Tooltip": "Homepage - Tooltip", "Homepage - Tooltip": "Homepage - Tooltip",
"ID card": "ID card",
"Input your email": "Input your email", "Input your email": "Input your email",
"Input your phone number": "Input your phone number", "Input your phone number": "Input your phone number",
"Is admin": "Is admin", "Is admin": "Is admin",
@ -410,6 +445,7 @@
"Title - Tooltip": "Title - Tooltip", "Title - Tooltip": "Title - Tooltip",
"Two passwords you typed do not match.": "Two passwords you typed do not match.", "Two passwords you typed do not match.": "Two passwords you typed do not match.",
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"input password": "input password" "input password": "input password"
}, },
@ -421,6 +457,8 @@
"Events - Tooltip": "Events - Tooltip", "Events - Tooltip": "Events - Tooltip",
"Headers": "Headers", "Headers": "Headers",
"Headers - Tooltip": "Headers - Tooltip", "Headers - Tooltip": "Headers - Tooltip",
"Is user extended": "Is user extended",
"Is user extended - Tooltip": "Is user extended - Tooltip",
"Method": "Method", "Method": "Method",
"Method - Tooltip": "Method - Tooltip", "Method - Tooltip": "Method - Tooltip",
"Name": "Name", "Name": "Name",

View File

@ -7,7 +7,7 @@
"Sign Up": "注册" "Sign Up": "注册"
}, },
"application": { "application": {
"Edit Application": "修改应用", "Edit Application": "编辑应用",
"Enable code signin": "启用验证码登录", "Enable code signin": "启用验证码登录",
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录", "Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
"Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话", "Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话",
@ -33,6 +33,29 @@
"Token format": "Access Token格式", "Token format": "Access Token格式",
"Token format - Tooltip": "Access Token格式" "Token format - Tooltip": "Access Token格式"
}, },
"cert": {
"Bit size": "位大小",
"Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "复制私钥",
"Copy public key": "复制公钥",
"Crypto algorithm": "加密算法",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "下载私钥",
"Download public key": "下载公钥",
"Edit Cert": "编辑证书",
"Expire in years": "有效期(年)",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"Private key": "私钥",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "私钥已成功复制到剪贴板",
"Public key": "公钥",
"Public key - Tooltip": "Public key - Tooltip",
"Public key copied to clipboard successfully": "公钥已成功复制到剪贴板",
"Scope": "用途",
"Scope - Tooltip": "Scope - Tooltip",
"Type": "类型",
"Type - Tooltip": "Type - Tooltip"
},
"code": { "code": {
"Code You Received": "验证码", "Code You Received": "验证码",
"Email code": "邮箱验证码", "Email code": "邮箱验证码",
@ -47,7 +70,7 @@
}, },
"forget": { "forget": {
"Account": "账号", "Account": "账号",
"Change Password": "修改密码", "Change Password": "编辑密码",
"Choose email or phone": "请选择邮箱或手机号验证", "Choose email or phone": "请选择邮箱或手机号验证",
"Confirm": "验证密码", "Confirm": "验证密码",
"Next Step": "下一步", "Next Step": "下一步",
@ -69,6 +92,7 @@
"Avatar - Tooltip": "向其他人展示的头像", "Avatar - Tooltip": "向其他人展示的头像",
"Back Home": "返回到首页", "Back Home": "返回到首页",
"Captcha": "人机验证码", "Captcha": "人机验证码",
"Certs": "证书",
"Client IP": "客户端IP", "Client IP": "客户端IP",
"Created time": "创建时间", "Created time": "创建时间",
"Default avatar": "默认头像", "Default avatar": "默认头像",
@ -79,7 +103,7 @@
"Display name": "显示名称", "Display name": "显示名称",
"Display name - Tooltip": "显示名称", "Display name - Tooltip": "显示名称",
"Down": "下移", "Down": "下移",
"Edit": "修改", "Edit": "编辑",
"Email": "电子邮箱", "Email": "电子邮箱",
"Email - Tooltip": "电子邮件:", "Email - Tooltip": "电子邮件:",
"Favicon - Tooltip": "网站的图标", "Favicon - Tooltip": "网站的图标",
@ -122,6 +146,7 @@
"Records": "日志", "Records": "日志",
"Request URI": "请求URI", "Request URI": "请求URI",
"Resources": "资源", "Resources": "资源",
"Roles": "角色",
"Save": "保存", "Save": "保存",
"Save & Exit": "保存 & 退出", "Save & Exit": "保存 & 退出",
"Signin URL": "登录URL", "Signin URL": "登录URL",
@ -148,27 +173,27 @@
"ldap": { "ldap": {
"Address": "地址", "Address": "地址",
"Admin": "管理员", "Admin": "管理员",
"Admin - Tooltip": "LDAP 服务器管理员的 CNID", "Admin - Tooltip": "LDAP服务器管理员的CNID",
"Admin Password": "密码", "Admin Password": "密码",
"Admin Password - Tooltip": "LDAP 服务器管理员密码", "Admin Password - Tooltip": "LDAP服务器管理员密码",
"Auto Sync": "自动同步", "Auto Sync": "自动同步",
"Auto Sync - Tooltip": "自动同步配置,为 0 时禁用", "Auto Sync - Tooltip": "自动同步配置,为0时禁用",
"Base DN": "基本 DN", "Base DN": "基本DN",
"Base DN - Tooltip": "LDAP 搜索时的基 DN", "Base DN - Tooltip": "LDAP搜索时的基 DN",
"CN": "CN", "CN": "CN",
"Edit LDAP": "编辑 LDAP", "Edit LDAP": "编辑LDAP",
"Email": "电子邮件", "Email": "电子邮件",
"Group Id": "组 Id", "Group Id": "组ID",
"ID": "ID", "ID": "ID",
"Last Sync": "最近同步", "Last Sync": "最近同步",
"Phone": "电话", "Phone": "电话",
"Server": "服务器", "Server": "服务器",
"Server Host": "域名", "Server Host": "域名",
"Server Host - Tooltip": "LDAP 服务器地址", "Server Host - Tooltip": "LDAP服务器地址",
"Server Name": "LDAP 服务器", "Server Name": "LDAP服务器",
"Server Name - Tooltip": "LDAP 服务器配置显示名称", "Server Name - Tooltip": "LDAP服务器配置显示名称",
"Server Port": "端口", "Server Port": "端口",
"Server Port - Tooltip": "LDAP 服务器端口号", "Server Port - Tooltip": "LDAP服务器端口号",
"Sync": "同步", "Sync": "同步",
"The Auto Sync option will sync all users to specify organization": "The Auto Sync option will sync all users to specify organization", "The Auto Sync option will sync all users to specify organization": "The Auto Sync option will sync all users to specify organization",
"UidNumber / Uid": "UidNumber / Uid" "UidNumber / Uid": "UidNumber / Uid"
@ -199,7 +224,7 @@
}, },
"organization": { "organization": {
"Default avatar": "默认头像", "Default avatar": "默认头像",
"Edit Organization": "修改组织", "Edit Organization": "编辑组织",
"Favicon": "图标", "Favicon": "图标",
"Soft deletion": "软删除", "Soft deletion": "软删除",
"Soft deletion - Tooltip": "启用后,删除用户信息时不会在数据库彻底清除,只会标记为已删除状态", "Soft deletion - Tooltip": "启用后,删除用户信息时不会在数据库彻底清除,只会标记为已删除状态",
@ -225,7 +250,7 @@
"Copy": "Copy", "Copy": "Copy",
"Domain": "域名", "Domain": "域名",
"Domain - Tooltip": "存储节点自定义域名", "Domain - Tooltip": "存储节点自定义域名",
"Edit Provider": "修改提供商", "Edit Provider": "编辑提供商",
"Email Content": "邮件内容", "Email Content": "邮件内容",
"Email Content - Tooltip": "邮件内容", "Email Content - Tooltip": "邮件内容",
"Email Title": "邮件标题", "Email Title": "邮件标题",
@ -246,7 +271,7 @@
"Name": "名称", "Name": "名称",
"Parse": "Parse", "Parse": "Parse",
"Parse Metadata successfully": "Parse Metadata successfully", "Parse Metadata successfully": "Parse Metadata successfully",
"Port": "端口", "Port": "端口",
"Port - Tooltip": "端口号", "Port - Tooltip": "端口号",
"Provider URL": "提供商URL", "Provider URL": "提供商URL",
"Provider URL - Tooltip": "提供商URL", "Provider URL - Tooltip": "提供商URL",
@ -292,8 +317,8 @@
"Application": "应用", "Application": "应用",
"Copy Link": "复制链接", "Copy Link": "复制链接",
"File name": "文件名", "File name": "文件名",
"File size": "文件大小", "File size": "大小",
"Format": "文件格式", "Format": "格式",
"Link copied to clipboard successfully": "链接已成功复制到剪贴板", "Link copied to clipboard successfully": "链接已成功复制到剪贴板",
"Parent": "属主", "Parent": "属主",
"Tag": "标签", "Tag": "标签",
@ -301,6 +326,13 @@
"Upload a file...": "上传文件...", "Upload a file...": "上传文件...",
"User": "用户" "User": "用户"
}, },
"role": {
"Edit Role": "编辑角色",
"Sub roles": "包含角色",
"Sub roles - Tooltip": "当前角色所包含的子角色",
"Sub users": "包含用户",
"Sub users - Tooltip": "当前角色所包含的子用户"
},
"signup": { "signup": {
"Accept": "阅读并接受", "Accept": "阅读并接受",
"Confirm": "确认密码", "Confirm": "确认密码",
@ -309,7 +341,9 @@
"Please accept the agreement!": "请先同意用户协议!", "Please accept the agreement!": "请先同意用户协议!",
"Please click the below button to sign in": "请点击下方按钮登录", "Please click the below button to sign in": "请点击下方按钮登录",
"Please confirm your password!": "请再次确认您的密码!", "Please confirm your password!": "请再次确认您的密码!",
"Please input the correct ID card number!": "请输入正确的身份证号!",
"Please input your Email!": "请输入您的电子邮箱!", "Please input your Email!": "请输入您的电子邮箱!",
"Please input your ID card number!": "请输入您的身份证号!",
"Please input your address!": "请输入您的地址!", "Please input your address!": "请输入您的地址!",
"Please input your affiliation!": "请输入您所在的工作单位!", "Please input your affiliation!": "请输入您所在的工作单位!",
"Please input your display name!": "请输入您的显示名称!", "Please input your display name!": "请输入您的显示名称!",
@ -338,7 +372,7 @@
"Database - Tooltip": "数据库名称", "Database - Tooltip": "数据库名称",
"Database type": "数据库类型", "Database type": "数据库类型",
"Database type - Tooltip": "数据库类型", "Database type - Tooltip": "数据库类型",
"Edit Syncer": "修改同步器", "Edit Syncer": "编辑同步器",
"Is hashed": "是否参与哈希计算", "Is hashed": "是否参与哈希计算",
"Sync interval": "同步间隔", "Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为分钟", "Sync interval - Tooltip": "单位为分钟",
@ -352,7 +386,7 @@
"token": { "token": {
"Access token": "Access token", "Access token": "Access token",
"Authorization code": "授权码", "Authorization code": "授权码",
"Edit Token": "修改令牌", "Edit Token": "编辑令牌",
"Expires in": "有效期", "Expires in": "有效期",
"Scope": "范围", "Scope": "范围",
"Token type": "令牌类型" "Token type": "令牌类型"
@ -371,10 +405,11 @@
"Code Sent": "验证码已发送", "Code Sent": "验证码已发送",
"Country/Region": "国家/地区", "Country/Region": "国家/地区",
"Country/Region - Tooltip": "国家/地区", "Country/Region - Tooltip": "国家/地区",
"Edit User": "修改用户", "Edit User": "编辑用户",
"Empty input!": "输入为空!", "Empty input!": "输入为空!",
"Homepage": "个人主页", "Homepage": "个人主页",
"Homepage - Tooltip": "个人主页链接", "Homepage - Tooltip": "个人主页链接",
"ID card": "身份证号",
"Input your email": "请输入邮箱", "Input your email": "请输入邮箱",
"Input your phone number": "输入手机号", "Input your phone number": "输入手机号",
"Is admin": "是管理员", "Is admin": "是管理员",
@ -388,7 +423,7 @@
"Link": "绑定", "Link": "绑定",
"Location": "城市", "Location": "城市",
"Location - Tooltip": "居住地址所在的城市", "Location - Tooltip": "居住地址所在的城市",
"Modify password...": "修改密码...", "Modify password...": "编辑密码...",
"New Email": "新邮箱", "New Email": "新邮箱",
"New Password": "新密码", "New Password": "新密码",
"New phone": "新手机号", "New phone": "新手机号",
@ -410,17 +445,20 @@
"Title - Tooltip": "在单位/公司的职务", "Title - Tooltip": "在单位/公司的职务",
"Two passwords you typed do not match.": "两次输入的密码不匹配。", "Two passwords you typed do not match.": "两次输入的密码不匹配。",
"Unlink": "解绑", "Unlink": "解绑",
"Upload (.xlsx)": "上传(.xlsx",
"Upload a photo": "上传头像", "Upload a photo": "上传头像",
"input password": "输入密码" "input password": "输入密码"
}, },
"webhook": { "webhook": {
"Content type": "Content type", "Content type": "Content type",
"Content type - Tooltip": "Content type", "Content type - Tooltip": "Content type",
"Edit Webhook": "修改Webhook", "Edit Webhook": "编辑Webhook",
"Events": "事件", "Events": "事件",
"Events - Tooltip": "事件", "Events - Tooltip": "事件",
"Headers": "协议头", "Headers": "协议头",
"Headers - Tooltip": "HTTP协议头键值对", "Headers - Tooltip": "HTTP协议头键值对",
"Is user extended": "扩展用户字段",
"Is user extended - Tooltip": "JSON里加入extendedUser来扩展用户字段",
"Method": "方法", "Method": "方法",
"Method - Tooltip": "HTTP方法", "Method - Tooltip": "HTTP方法",
"Name": "名称", "Name": "名称",

View File

@ -5190,6 +5190,11 @@ file-loader@6.1.1:
loader-utils "^2.0.0" loader-utils "^2.0.0"
schema-utils "^3.0.0" schema-utils "^3.0.0"
file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
file-uri-to-path@1.0.0: file-uri-to-path@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"

43
xlsx/xlsx.go Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2021 The casbin 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 xlsx
import (
"github.com/casbin/casdoor/util"
"github.com/tealeg/xlsx"
)
func ReadXlsxFile(fileId string) [][]string {
path := util.GetUploadXlsxPath(fileId)
file, err := xlsx.OpenFile(path)
if err != nil {
panic(err)
}
res := [][]string{}
for _, sheet := range file.Sheets {
for _, row := range sheet.Rows {
line := []string{}
for _, cell := range row.Cells {
text := cell.String()
line = append(line, text)
}
res = append(res, line)
}
break
}
return res
}

22
xlsx/xlsx_test.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2021 The casbin 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 xlsx
import "testing"
func TestReadSheet(t *testing.T) {
ticket := ReadXlsxFile("../../tmpFiles/example")
println(ticket)
}