Compare commits

...

39 Commits

Author SHA1 Message Date
Baihhh
aee3ea4981 feat: improve TermsOfUse UI in mobile (#2106)
* style: Mobile interface adaptation

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

* Update index.css

---------

Signed-off-by: baihhh <2542274498@qq.com>
Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-07-23 15:28:13 +08:00
Yang Luo
516f4b7569 Fix response of /api/get-sorted-users and /api/get-user-count 2023-07-23 14:46:38 +08:00
UsherFall
7d7ca10481 fix: hide fields of minio storage provider (#2115)
* feat: hide field of minio storage provider

* feat: hide field of domain in minio storage provider
2023-07-23 14:40:30 +08:00
UsherFall
a9d4978a0f chore: hide fields of local file system storage provider (#2109)
* style: adjust local file system storage

* style: disable domain when use local file system
2023-07-23 11:48:15 +08:00
Yang Luo
09f40bb5ce Fix id of "/api/get-resource" API 2023-07-23 11:33:48 +08:00
Yaodong Yu
a6f803aff1 feat: refactor code to use responseOK everywhere (#2111)
* refactor: use responseOK return frontend format json data

* revert handle error

* revert handle error
2023-07-23 09:49:16 +08:00
Yang Luo
fc9528be43 Add createDatabaseForPostgres() 2023-07-22 16:19:13 +08:00
imp2002
58e8f9f90b feat: fix Effect in Casbin rule (#2103)
* fix: Add `Effect` to Casbin rule of role

fix: https://github.com/casdoor/casdoor/issues/2102

* Update permission_enforcer.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-07-21 18:01:37 +08:00
Yang Luo
e850e33f37 Fix error message of missing cert when login 2023-07-20 19:45:22 +08:00
haiwu
d7110ff8bf feat: support MetaMask provider (#2084)
* feat: add metamask provider

* feat: add eth login

* feat: check eth sign

* feat: finish metamask signin/signup

* feat: support MetaMask provider link/unlink

* feat: update web/craco.config.js to handle polyfill

* feat: gofumpt idp/metamask.go

* feat: update MetaMask logo path

* feat: support MetaMask avatar
2023-07-20 17:51:36 +08:00
f923a8f0d7 fix: provide detailed description of ldap in swagger (#2094)
* provide detailed description of ldap in swagger

* modify the directory of swagger

fix: provide detailed description of ldap in swagger
2023-07-20 12:32:48 +08:00
Yang Luo
7bfb74ba18 Fix typo 2023-07-19 19:34:43 +08:00
Yang Luo
38f031bc86 Show access secret if isAdminOrSelf is true in get-user and get-account APIs 2023-07-19 19:14:53 +08:00
Yang Luo
5c441d195c Add Effect to Casbin rule of add-permission 2023-07-19 18:52:22 +08:00
Yaodong Yu
0639564d27 fix: check group name cannot be same as organization name (#2090) 2023-07-19 11:37:28 +08:00
Yang Luo
6c647818ca feat: add "Sender number" input for Twilio SMS provider 2023-07-18 22:46:56 +08:00
Yaodong Yu
8bc73d17aa feat: fix bug that themeEditor can not load saved theme data (#2085) 2023-07-17 22:57:55 +08:00
Yang Luo
1f37c80177 feat: refactor code to add getStorageProvider() 2023-07-17 15:59:37 +08:00
Yaodong Yu
7924fca403 fix: hidden bug of "like" query (#2082) 2023-07-16 17:11:32 +08:00
Yang Luo
bd06996bab Improve CorsFilter for login API 2023-07-15 19:29:48 +08:00
Yang Luo
19ab168b12 Fix panic in func (c *ApiController) GetUser() if no user exists in DB 2023-07-14 20:57:59 +08:00
UsherFall
854a74b73e feat: fix the error when user uploads avatar to minio (https) (#2078)
* fix: Error reported when user uploads avatar to minio (https)

* Update provider.go

---------

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

* fix: clean session when leave promptpage

* fix: css

* fix: force enable mfa

* fix: add prompt rule

* fix: refactor directory structure

* fix: prompt notification

* fix: fix some bug and clean code

* fix: rebase

* fix: improve notification

* fix: i18n

* fix: router

* fix: prompt

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

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

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

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

---------

Signed-off-by: baihhh <2542274498@qq.com>
2023-07-06 20:36:32 +08:00
YunShu
96250aa70a docs: replace gitter links with discord (#2041) 2023-07-06 18:16:16 +08:00
129 changed files with 3028 additions and 1046 deletions

View File

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

View File

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

View File

@@ -368,9 +368,11 @@ func (c *ApiController) GetAccount() {
return
}
user.Permissions = object.GetMaskedPermissions(user.Permissions)
user.Roles = object.GetMaskedRoles(user.Roles)
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
if user != nil {
user.Permissions = object.GetMaskedPermissions(user.Permissions)
user.Roles = object.GetMaskedRoles(user.Roles)
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
}
organization, err := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
if err != nil {
@@ -378,7 +380,8 @@ func (c *ApiController) GetAccount() {
return
}
u, err := object.GetMaskedUser(user)
isAdminOrSelf := c.IsAdminOrSelf(user)
u, err := object.GetMaskedUser(user, isAdminOrSelf)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -48,14 +48,11 @@ func (c *ApiController) GetApplications() {
} else {
applications, err = object.GetOrganizationApplications(owner, organization)
}
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedApplications(applications, userId)
c.ServeJSON()
c.ResponseOk(object.GetMaskedApplications(applications, userId))
} else {
limit := util.ParseInt(limit)
count, err := object.GetApplicationCount(owner, field, value)
@@ -86,14 +83,14 @@ func (c *ApiController) GetApplications() {
func (c *ApiController) GetApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id")
app, err := object.GetApplication(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedApplication(app, userId)
c.ServeJSON()
c.ResponseOk(object.GetMaskedApplication(app, userId))
}
// GetUserApplication
@@ -106,25 +103,24 @@ func (c *ApiController) GetApplication() {
func (c *ApiController) GetUserApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id")
user, err := object.GetUser(id)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), id))
return
}
app, err := object.GetApplicationByUser(user)
application, err := object.GetApplicationByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetMaskedApplication(app, userId)
c.ServeJSON()
c.ResponseOk(object.GetMaskedApplication(application, userId))
}
// GetOrganizationApplications
@@ -157,8 +153,7 @@ func (c *ApiController) GetOrganizationApplications() {
return
}
c.Data["json"] = object.GetMaskedApplications(applications, userId)
c.ServeJSON()
c.ResponseOk(object.GetMaskedApplications(applications, userId))
} else {
limit := util.ParseInt(limit)

View File

@@ -78,12 +78,6 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
}
}
if form.Password != "" && user.IsMfaEnabled() {
c.setMfaSessionData(&object.MfaSessionData{UserId: userId})
resp = &Response{Status: object.NextMfa, Data: user.GetPreferredMfaProps(true)}
return
}
if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
@@ -129,6 +123,11 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
return
}
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]string{"redirectUrl": redirectUrl, "method": method}}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else if form.Type == ResponseTypeCas {
// not oauth but CAS SSO protocol
service := c.Input().Get("service")
@@ -141,11 +140,11 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
resp.Data = st
}
}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else {
resp = wrapErrorResponse(fmt.Errorf("unknown response type: %s", form.Type))
}
@@ -353,17 +352,26 @@ func (c *ApiController) Login() {
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
organization, err := object.GetOrganizationByUser(user)
if err != nil {
c.ResponseError(err.Error())
}
if user != nil && organization.HasRequiredMfa() && !user.IsMfaEnabled() {
resp.Msg = object.RequiredMfa
if object.IsNeedPromptMfa(organization, user) {
// The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId())
c.ResponseOk(object.RequiredMfa)
return
}
if user.IsMfaEnabled() {
c.setMfaUserSession(user.GetId())
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
@@ -414,7 +422,7 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
} else if provider.Category == "OAuth" {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idProvider := idp.GetIdProvider(idpInfo, authForm.RedirectUri)
@@ -457,7 +465,7 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
} else if provider.Category == "OAuth" {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
user, err = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
@@ -478,7 +486,7 @@ func (c *ApiController) Login() {
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else if provider.Category == "OAuth" {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// Sign up via OAuth
if application.EnableLinkWithEmail {
if userInfo.Email != "" {
@@ -649,13 +657,16 @@ func (c *ApiController) Login() {
resp = &Response{Status: "error", Msg: "Failed to link user account", Data: isLinked}
}
}
} else if c.getMfaSessionData() != nil {
mfaSession := c.getMfaSessionData()
user, err := object.GetUser(mfaSession.UserId)
} else if c.getMfaUserSession() != "" {
user, err := object.GetUser(c.getMfaUserSession())
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError("expired user session")
return
}
if authForm.Passcode != "" {
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
@@ -669,13 +680,15 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
}
if authForm.RecoveryCode != "" {
} else if authForm.RecoveryCode != "" {
err = object.MfaRecover(user, authForm.RecoveryCode)
if err != nil {
c.ResponseError(err.Error())
return
}
} else {
c.ResponseError("missing passcode or recovery code")
return
}
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
@@ -690,6 +703,7 @@ func (c *ApiController) Login() {
}
resp = c.HandleLoggedIn(application, user, &authForm)
c.setMfaUserSession("")
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization

View File

@@ -55,6 +55,18 @@ func (c *ApiController) IsAdmin() bool {
return isGlobalAdmin || user.IsAdmin
}
func (c *ApiController) IsAdminOrSelf(user2 *object.User) bool {
isGlobalAdmin, user := c.isGlobalAdmin()
if isGlobalAdmin || (user != nil && user.IsAdmin) {
return true
}
if user.Owner == user2.Owner && user.Name == user2.Name {
return true
}
return false
}
func (c *ApiController) isGlobalAdmin() (bool, *object.User) {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {
@@ -178,24 +190,16 @@ func (c *ApiController) SetSessionData(s *SessionData) {
c.SetSession("SessionData", util.StructToJson(s))
}
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) {
if data == nil {
c.SetSession(object.MfaSessionUserId, nil)
return
}
c.SetSession(object.MfaSessionUserId, data.UserId)
func (c *ApiController) setMfaUserSession(userId string) {
c.SetSession(object.MfaSessionUserId, userId)
}
func (c *ApiController) getMfaSessionData() *object.MfaSessionData {
userId := c.GetSession(object.MfaSessionUserId)
func (c *ApiController) getMfaUserSession() string {
userId := c.Ctx.Input.CruSession.Get(object.MfaSessionUserId)
if userId == nil {
return nil
return ""
}
data := &object.MfaSessionData{
UserId: userId.(string),
}
return data
return userId.(string)
}
func (c *ApiController) setExpireForSession() {

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetCerts() {
return
}
c.Data["json"] = maskedCerts
c.ServeJSON()
c.ResponseOk(maskedCerts)
} else {
limit := util.ParseInt(limit)
count, err := object.GetCertCount(owner, field, value)
@@ -87,8 +86,7 @@ func (c *ApiController) GetGlobleCerts() {
return
}
c.Data["json"] = maskedCerts
c.ServeJSON()
c.ResponseOk(maskedCerts)
} else {
limit := util.ParseInt(limit)
count, err := object.GetGlobalCertsCount(field, value)
@@ -123,8 +121,7 @@ func (c *ApiController) GetCert() {
return
}
c.Data["json"] = object.GetMaskedCert(cert)
c.ServeJSON()
c.ResponseOk(object.GetMaskedCert(cert))
}
// UpdateCert

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetChats() {
return
}
c.Data["json"] = maskedChats
c.ServeJSON()
c.ResponseOk(maskedChats)
} else {
limit := util.ParseInt(limit)
count, err := object.GetChatCount(owner, field, value)
@@ -82,8 +81,7 @@ func (c *ApiController) GetChat() {
return
}
c.Data["json"] = maskedChat
c.ServeJSON()
c.ResponseOk(maskedChat)
}
// UpdateChat

View File

@@ -82,9 +82,9 @@ func (c *ApiController) GetGroup() {
group, err := object.GetGroup(id)
if err != nil {
c.ResponseError(err.Error())
} else {
c.ResponseOk(group)
return
}
c.ResponseOk(group)
}
// UpdateGroup

View File

@@ -38,8 +38,11 @@ type LdapSyncResp struct {
}
// GetLdapUsers
// @Tag Account API
// @Title GetLdapser
// @Tag Account API
// @Description get ldap users
// Param id string true "id"
// @Success 200 {object} LdapResp The Response object
// @router /get-ldap-users [get]
func (c *ApiController) GetLdapUsers() {
id := c.Input().Get("id")
@@ -94,18 +97,24 @@ func (c *ApiController) GetLdapUsers() {
}
// GetLdaps
// @Tag Account API
// @Title GetLdaps
// @Tag Account API
// @Description get ldaps
// @Param owner query string false "owner"
// @Success 200 {array} object.Ldap The Response object
// @router /get-ldaps [get]
func (c *ApiController) GetLdaps() {
owner := c.Input().Get("owner")
c.ResponseOk(object.GetLdaps(owner))
c.ResponseOk(object.GetMaskedLdaps(object.GetLdaps(owner)))
}
// GetLdap
// @Tag Account API
// @Title GetLdap
// @Tag Account API
// @Description get ldap
// @Param id query string true "id"
// @Success 200 {object} object.Ldap The Response object
// @router /get-ldap [get]
func (c *ApiController) GetLdap() {
id := c.Input().Get("id")
@@ -116,12 +125,20 @@ func (c *ApiController) GetLdap() {
}
_, name := util.GetOwnerAndNameFromId(id)
c.ResponseOk(object.GetLdap(name))
ldap, err := object.GetLdap(name)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedLdap(ldap))
}
// AddLdap
// @Tag Account API
// @Title AddLdap
// @Tag Account API
// @Description add ldap
// @Param body body object.Ldap true "The details of the ldap"
// @Success 200 {object} controllers.Response The Response object
// @router /add-ldap [post]
func (c *ApiController) AddLdap() {
var ldap object.Ldap
@@ -160,8 +177,11 @@ func (c *ApiController) AddLdap() {
}
// UpdateLdap
// @Tag Account API
// @Title UpdateLdap
// @Tag Account API
// @Description update ldap
// @Param body body object.Ldap true "The details of the ldap"
// @Success 200 {object} controllers.Response The Response object
// @router /update-ldap [post]
func (c *ApiController) UpdateLdap() {
var ldap object.Ldap
@@ -198,8 +218,11 @@ func (c *ApiController) UpdateLdap() {
}
// DeleteLdap
// @Tag Account API
// @Title DeleteLdap
// @Tag Account API
// @Description delete ldap
// @Param body body object.Ldap true "The details of the ldap"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-ldap [post]
func (c *ApiController) DeleteLdap() {
var ldap object.Ldap
@@ -222,12 +245,16 @@ func (c *ApiController) DeleteLdap() {
}
// SyncLdapUsers
// @Tag Account API
// @Title SyncLdapUsers
// @Tag Account API
// @Description sync ldap users
// @Param id query string true "id"
// @Success 200 {object} LdapSyncResp The Response object
// @router /sync-ldap-users [post]
func (c *ApiController) SyncLdapUsers() {
owner := c.Input().Get("owner")
ldapId := c.Input().Get("ldapId")
id := c.Input().Get("id")
owner, ldapId := util.GetOwnerAndNameFromId(id)
var users []object.LdapUser
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
if err != nil {

View File

@@ -57,8 +57,7 @@ func (c *ApiController) GetMessages() {
return
}
c.Data["json"] = object.GetMaskedMessages(messages)
c.ServeJSON()
c.ResponseOk(object.GetMaskedMessages(messages))
} else {
limit := util.ParseInt(limit)
count, err := object.GetMessageCount(owner, organization, field, value)
@@ -94,8 +93,7 @@ func (c *ApiController) GetMessage() {
return
}
c.Data["json"] = object.GetMaskedMessage(message)
c.ServeJSON()
c.ResponseOk(message)
}
func (c *ApiController) ResponseErrorStream(errorText string) {

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetModels() {
return
}
c.Data["json"] = models
c.ServeJSON()
c.ResponseOk(models)
} else {
limit := util.ParseInt(limit)
count, err := object.GetModelCount(owner, field, value)
@@ -82,8 +81,7 @@ func (c *ApiController) GetModel() {
return
}
c.Data["json"] = model
c.ServeJSON()
c.ResponseOk(model)
}
// UpdateModel

View File

@@ -55,8 +55,7 @@ func (c *ApiController) GetOrganizations() {
return
}
c.Data["json"] = maskedOrganizations
c.ServeJSON()
c.ResponseOk(maskedOrganizations)
} else {
if !isGlobalAdmin {
maskedOrganizations, err := object.GetMaskedOrganizations(object.GetOrganizations(owner, c.getCurrentUser().Owner))

View File

@@ -46,8 +46,7 @@ func (c *ApiController) GetPayments() {
return
}
c.Data["json"] = payments
c.ServeJSON()
c.ResponseOk(payments)
} else {
limit := util.ParseInt(limit)
count, err := object.GetPaymentCount(owner, organization, field, value)
@@ -106,8 +105,7 @@ func (c *ApiController) GetPayment() {
return
}
c.Data["json"] = payment
c.ServeJSON()
c.ResponseOk(payment)
}
// UpdatePayment

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetPermissions() {
return
}
c.Data["json"] = permissions
c.ServeJSON()
c.ResponseOk(permissions)
} else {
limit := util.ParseInt(limit)
count, err := object.GetPermissionCount(owner, field, value)
@@ -85,7 +84,6 @@ func (c *ApiController) GetPermissionsBySubmitter() {
}
c.ResponseOk(permissions, len(permissions))
return
}
// GetPermissionsByRole
@@ -104,7 +102,6 @@ func (c *ApiController) GetPermissionsByRole() {
}
c.ResponseOk(permissions, len(permissions))
return
}
// GetPermission
@@ -123,8 +120,7 @@ func (c *ApiController) GetPermission() {
return
}
c.Data["json"] = permission
c.ServeJSON()
c.ResponseOk(permission)
}
// UpdatePermission

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetPlans() {
return
}
c.Data["json"] = plans
c.ServeJSON()
c.ResponseOk(plans)
} else {
limit := util.ParseInt(limit)
count, err := object.GetPlanCount(owner, field, value)

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetPricings() {
return
}
c.Data["json"] = pricings
c.ServeJSON()
c.ResponseOk(pricings)
} else {
limit := util.ParseInt(limit)
count, err := object.GetPricingCount(owner, field, value)
@@ -82,8 +81,7 @@ func (c *ApiController) GetPricing() {
return
}
c.Data["json"] = pricing
c.ServeJSON()
c.ResponseOk(pricing)
}
// UpdatePricing

View File

@@ -46,8 +46,7 @@ func (c *ApiController) GetProducts() {
return
}
c.Data["json"] = products
c.ServeJSON()
c.ResponseOk(products)
} else {
limit := util.ParseInt(limit)
count, err := object.GetProductCount(owner, field, value)

View File

@@ -51,8 +51,7 @@ func (c *ApiController) GetRecords() {
return
}
c.Data["json"] = records
c.ServeJSON()
c.ResponseOk(records)
} else {
limit := util.ParseInt(limit)
if c.IsGlobalAdmin() && organizationName != "" {
@@ -99,8 +98,7 @@ func (c *ApiController) GetRecordsByFilter() {
return
}
c.Data["json"] = records
c.ServeJSON()
c.ResponseOk(records)
}
// AddRecord

View File

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

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetRoles() {
return
}
c.Data["json"] = roles
c.ServeJSON()
c.ResponseOk(roles)
} else {
limit := util.ParseInt(limit)
count, err := object.GetRoleCount(owner, field, value)
@@ -82,8 +81,7 @@ func (c *ApiController) GetRole() {
return
}
c.Data["json"] = role
c.ServeJSON()
c.ResponseOk(role)
}
// UpdateRole

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetSessions() {
return
}
c.Data["json"] = sessions
c.ServeJSON()
c.ResponseOk(sessions)
} else {
limit := util.ParseInt(limit)
count, err := object.GetSessionCount(owner, field, value)
@@ -81,8 +80,7 @@ func (c *ApiController) GetSingleSession() {
return
}
c.Data["json"] = session
c.ServeJSON()
c.ResponseOk(session)
}
// UpdateSession
@@ -161,7 +159,5 @@ func (c *ApiController) IsSessionDuplicated() {
return
}
c.Data["json"] = &Response{Status: "ok", Msg: "", Data: isUserSessionDuplicated}
c.ServeJSON()
c.ResponseOk(isUserSessionDuplicated)
}

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetSubscriptions() {
return
}
c.Data["json"] = subscriptions
c.ServeJSON()
c.ResponseOk(subscriptions)
} else {
limit := util.ParseInt(limit)
count, err := object.GetSubscriptionCount(owner, field, value)
@@ -82,8 +81,7 @@ func (c *ApiController) GetSubscription() {
return
}
c.Data["json"] = subscription
c.ServeJSON()
c.ResponseOk(subscription)
}
// UpdateSubscription

View File

@@ -46,8 +46,7 @@ func (c *ApiController) GetSyncers() {
return
}
c.Data["json"] = organizationSyncers
c.ServeJSON()
c.ResponseOk(organizationSyncers)
} else {
limit := util.ParseInt(limit)
count, err := object.GetSyncerCount(owner, organization, field, value)
@@ -83,8 +82,7 @@ func (c *ApiController) GetSyncer() {
return
}
c.Data["json"] = syncer
c.ServeJSON()
c.ResponseOk(syncer)
}
// UpdateSyncer

View File

@@ -47,8 +47,7 @@ func (c *ApiController) GetTokens() {
return
}
c.Data["json"] = token
c.ServeJSON()
c.ResponseOk(token)
} else {
limit := util.ParseInt(limit)
count, err := object.GetTokenCount(owner, organization, field, value)
@@ -83,8 +82,7 @@ func (c *ApiController) GetToken() {
return
}
c.Data["json"] = token
c.ServeJSON()
c.ResponseOk(token)
}
// UpdateToken

View File

@@ -45,8 +45,7 @@ func (c *ApiController) GetGlobalUsers() {
return
}
c.Data["json"] = maskedUsers
c.ServeJSON()
c.ResponseOk(maskedUsers)
} else {
limit := util.ParseInt(limit)
count, err := object.GetGlobalUserCount(field, value)
@@ -106,8 +105,7 @@ func (c *ApiController) GetUsers() {
return
}
c.Data["json"] = maskedUsers
c.ServeJSON()
c.ResponseOk(maskedUsers)
} else {
limit := util.ParseInt(limit)
count, err := object.GetUserCount(owner, field, value, groupName)
@@ -198,21 +196,24 @@ func (c *ApiController) GetUser() {
return
}
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
if user != nil {
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
}
err = object.ExtendUserWithRolesAndPermissions(user)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedUser, err := object.GetMaskedUser(user)
isAdminOrSelf := c.IsAdminOrSelf(user)
maskedUser, err := object.GetMaskedUser(user, isAdminOrSelf)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedUser
c.ServeJSON()
c.ResponseOk(maskedUser)
}
// UpdateUser
@@ -509,8 +510,7 @@ func (c *ApiController) GetSortedUsers() {
return
}
c.Data["json"] = maskedUsers
c.ServeJSON()
c.ResponseOk(maskedUsers)
}
// GetUserCount
@@ -537,8 +537,7 @@ func (c *ApiController) GetUserCount() {
return
}
c.Data["json"] = count
c.ServeJSON()
c.ResponseOk(count)
}
// AddUserkeys

View File

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

View File

@@ -46,8 +46,7 @@ func (c *ApiController) GetWebhooks() {
return
}
c.Data["json"] = webhooks
c.ServeJSON()
c.ResponseOk(webhooks)
} else {
limit := util.ParseInt(limit)
count, err := object.GetWebhookCount(owner, organization, field, value)
@@ -84,8 +83,7 @@ func (c *ApiController) GetWebhook() {
return
}
c.Data["json"] = webhook
c.ServeJSON()
c.ResponseOk(webhook)
}
// UpdateWebhook

View File

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

2
go.sum
View File

@@ -546,8 +546,6 @@ github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sashabaranov/go-openai v1.9.1 h1:3N52HkJKo9Zlo/oe1AVv5ZkCOny0ra58/ACvAxkN3MM=
github.com/sashabaranov/go-openai v1.9.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.12.0 h1:aRNHH0gtVfrpIaEolD0sWrLLRnYQNK4cH/bIAHwL8Rk=
github.com/sashabaranov/go-openai v1.12.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=

80
idp/metamask.go Normal file
View File

@@ -0,0 +1,80 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"golang.org/x/oauth2"
)
const Web3AuthTokenKey = "web3AuthToken"
type MetaMaskIdProvider struct {
Client *http.Client
}
type Web3AuthToken struct {
Address string `json:"address"`
Nonce string `json:"nonce"`
CreateAt uint64 `json:"createAt"`
TypedData string `json:"typedData"`
Signature string `json:"signature"` // signature for typed data
}
func NewMetaMaskIdProvider() *MetaMaskIdProvider {
idp := &MetaMaskIdProvider{}
return idp
}
func (idp *MetaMaskIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *MetaMaskIdProvider) GetToken(code string) (*oauth2.Token, error) {
web3AuthToken := Web3AuthToken{}
if err := json.Unmarshal([]byte(code), &web3AuthToken); err != nil {
return nil, err
}
token := &oauth2.Token{
AccessToken: web3AuthToken.Signature,
TokenType: "Bearer",
Expiry: time.Now().AddDate(0, 1, 0),
}
token = token.WithExtra(map[string]interface{}{
Web3AuthTokenKey: web3AuthToken,
})
return token, nil
}
func (idp *MetaMaskIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
// TODO use "github.com/ethereum/go-ethereum" to check address's eth balance or transaction
web3AuthToken, ok := token.Extra(Web3AuthTokenKey).(Web3AuthToken)
if !ok {
return nil, errors.New("invalid web3AuthToken")
}
userInfo := &UserInfo{
Id: web3AuthToken.Address,
Username: web3AuthToken.Address,
DisplayName: web3AuthToken.Address,
AvatarUrl: fmt.Sprintf("metamask:%v", web3AuthToken.Address),
}
return userInfo, nil
}

View File

@@ -109,6 +109,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) IdProvider {
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
case "Bilibili":
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
case "MetaMask":
return NewMetaMaskIdProvider()
default:
if isGothSupport(idpInfo.Type) {
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)

View File

@@ -15,8 +15,10 @@
package object
import (
"database/sql"
"fmt"
"runtime"
"strings"
"github.com/beego/beego"
"github.com/casdoor/casdoor/conf"
@@ -46,6 +48,11 @@ func InitConfig() {
}
func InitAdapter() {
err := createDatabaseForPostgres(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
if err != nil {
panic(err)
}
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
@@ -96,7 +103,32 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) *Adapte
return a
}
func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error {
if driverName == "postgres" {
db, err := sql.Open(driverName, dataSourceName)
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName))
if err != nil {
if !strings.Contains(err.Error(), "already exists") {
return err
}
}
return nil
} else {
return nil
}
}
func (a *Adapter) CreateDatabase() error {
if a.driverName == "postgres" {
return nil
}
engine, err := xorm.NewEngine(a.driverName, a.dataSourceName)
if err != nil {
return err

View File

@@ -107,6 +107,11 @@ func UpdateGroup(id string, group *Group) (bool, error) {
return false, err
}
err = checkGroupName(group.Name)
if err != nil {
return false, err
}
if name != group.Name {
err := GroupChangeTrigger(name, group.Name)
if err != nil {
@@ -123,6 +128,11 @@ func UpdateGroup(id string, group *Group) (bool, error) {
}
func AddGroup(group *Group) (bool, error) {
err := checkGroupName(group.Name)
if err != nil {
return false, err
}
affected, err := adapter.Engine.Insert(group)
if err != nil {
return false, err
@@ -168,6 +178,17 @@ func DeleteGroup(group *Group) (bool, error) {
return affected != 0, nil
}
func checkGroupName(name string) error {
exist, err := adapter.Engine.Exist(&Organization{Owner: "admin", Name: name})
if err != nil {
return err
}
if exist {
return errors.New("group name can't be same as the organization name")
}
return nil
}
func (group *Group) GetId() string {
return fmt.Sprintf("%s/%s", group.Owner, group.Name)
}
@@ -225,7 +246,7 @@ func GetGroupUserCount(groupName string, field, value string) (int64, error) {
func GetPaginationGroupUsers(groupName string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
users := []*User{}
session := adapter.Engine.Table("user").
Where(builder.Like{"`groups`", groupName})
Where(builder.Like{"`groups`", groupName + "\""})
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
@@ -255,7 +276,7 @@ func GetPaginationGroupUsers(groupName string, offset, limit int, field, value,
func GetGroupUsers(groupName string) ([]*User, error) {
users := []*User{}
err := adapter.Engine.Table("user").
Where(builder.Like{"`groups`", groupName}).
Where(builder.Like{"`groups`", groupName + "\""}).
Find(&users)
if err != nil {
return nil, err

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -104,29 +104,16 @@ func getPolicies(permission *Permission) [][]string {
permissionId := permission.GetId()
domainExist := len(permission.Domains) > 0
for _, user := range permission.Users {
usersAndRoles := append(permission.Users, permission.Roles...)
for _, userOrRole := range usersAndRoles {
for _, resource := range permission.Resources {
for _, action := range permission.Actions {
if domainExist {
for _, domain := range permission.Domains {
policies = append(policies, []string{user, domain, resource, strings.ToLower(action), "", permissionId})
policies = append(policies, []string{userOrRole, domain, resource, strings.ToLower(action), strings.ToLower(permission.Effect), permissionId})
}
} else {
policies = append(policies, []string{user, resource, strings.ToLower(action), "", "", permissionId})
}
}
}
}
for _, role := range permission.Roles {
for _, resource := range permission.Resources {
for _, action := range permission.Actions {
if domainExist {
for _, domain := range permission.Domains {
policies = append(policies, []string{role, domain, resource, strings.ToLower(action), "", permissionId})
}
} else {
policies = append(policies, []string{role, resource, strings.ToLower(action), "", "", permissionId})
policies = append(policies, []string{userOrRole, resource, strings.ToLower(action), strings.ToLower(permission.Effect), "", permissionId})
}
}
}

View File

@@ -229,7 +229,7 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
session = session.Omit("client_secret2")
}
if provider.Type != "Keycloak" {
if provider.Type == "Tencent Cloud COS" {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
}
@@ -243,7 +243,7 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
}
func AddProvider(provider *Provider) (bool, error) {
if provider.Type != "Keycloak" {
if provider.Type == "Tencent Cloud COS" {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
}

View File

@@ -49,7 +49,7 @@ func (pi *ProviderItem) IsProviderVisible() bool {
if pi.Provider == nil {
return false
}
return pi.Provider.Category == "OAuth" || pi.Provider.Category == "SAML"
return pi.Provider.Category == "OAuth" || pi.Provider.Category == "SAML" || pi.Provider.Category == "Web3"
}
func (pi *ProviderItem) isProviderPrompted() bool {

View File

@@ -161,7 +161,8 @@ func SendWebhooks(record *Record) error {
if matched {
if webhook.IsUserExtended {
user, err := GetMaskedUser(getUser(record.Organization, record.User))
user, err := getUser(record.Organization, record.User)
user, err = GetMaskedUser(user, false, err)
if err != nil {
return err
}

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
@@ -76,6 +77,10 @@ func GetPaginationResources(owner, user string, offset, limit int, field, value,
}
func getResource(owner string, name string) (*Resource, error) {
if !strings.HasPrefix(name, "/") {
name = "/" + name
}
resource := Resource{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&resource)
if err != nil {

View File

@@ -42,7 +42,11 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
return err
}
if provider.Type == sender.Aliyun {
if provider.Type == sender.Twilio {
if provider.AppId != "" {
phoneNumbers = append([]string{provider.AppId}, phoneNumbers...)
}
} else if provider.Type == sender.Aliyun {
for i, number := range phoneNumbers {
phoneNumbers[i] = strings.TrimPrefix(number, "+86")
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/storage"
"github.com/casdoor/casdoor/util"
"github.com/casdoor/oss"
)
var isCloudIntranet bool
@@ -102,11 +103,11 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
return fileUrl, objectKey
}
func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer, lang string) (string, string, error) {
func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface, error) {
endpoint := getProviderEndpoint(provider)
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint)
if storageProvider == nil {
return "", "", fmt.Errorf(i18n.Translate(lang, "storage:The provider type: %s is not supported"), provider.Type)
return nil, fmt.Errorf(i18n.Translate(lang, "storage:The provider type: %s is not supported"), provider.Type)
}
if provider.Domain == "" {
@@ -114,9 +115,18 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
UpdateProvider(provider.GetId(), provider)
}
return storageProvider, nil
}
func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer, lang string) (string, string, error) {
storageProvider, err := getStorageProvider(provider, lang)
if err != nil {
return "", "", err
}
fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true)
_, err := storageProvider.Put(objectKey, fileBuffer)
_, err = storageProvider.Put(objectKey, fileBuffer)
if err != nil {
return "", "", err
}
@@ -154,15 +164,9 @@ func DeleteFile(provider *Provider, objectKey string, lang string) error {
return fmt.Errorf(i18n.Translate(lang, "storage:The objectKey: %s is not allowed"), objectKey)
}
endpoint := getProviderEndpoint(provider)
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint)
if storageProvider == nil {
return fmt.Errorf(i18n.Translate(lang, "storage:The provider type: %s is not supported"), provider.Type)
}
if provider.Domain == "" {
provider.Domain = storageProvider.GetEndpoint()
UpdateProvider(provider.GetId(), provider)
storageProvider, err := getStorageProvider(provider, lang)
if err != nil {
return err
}
return storageProvider.Delete(objectKey)

View File

@@ -281,6 +281,14 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
return "", "", "", err
}
if cert == nil {
if application.Cert == "" {
return "", "", "", fmt.Errorf("The cert field of the application \"%s\" should not be empty", application.GetId())
} else {
return "", "", "", fmt.Errorf("The cert \"%s\" does not exist", application.Cert)
}
}
// RSA private key
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey))
if err != nil {

View File

@@ -156,12 +156,13 @@ type User struct {
Yammer string `xorm:"yammer varchar(100)" json:"yammer"`
Yandex string `xorm:"yandex varchar(100)" json:"yandex"`
Zoom string `xorm:"zoom varchar(100)" json:"zoom"`
MetaMask string `xorm:"metamask varchar(100)" json:"metamask"`
Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret,omitempty"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
@@ -418,7 +419,7 @@ func GetUserNoCheck(id string) (*User, error) {
return getUser(owner, name)
}
func GetMaskedUser(user *User, errs ...error) (*User, error) {
func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
@@ -430,9 +431,13 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
if user.Password != "" {
user.Password = "***"
}
if user.AccessSecret != "" {
user.AccessSecret = "***"
if !isAdminOrSelf {
if user.AccessSecret != "" {
user.AccessSecret = "***"
}
}
if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts {
manageAccount.Password = "***"
@@ -456,7 +461,7 @@ func GetMaskedUsers(users []*User, errs ...error) ([]*User, error) {
var err error
for _, user := range users {
user, err = GetMaskedUser(user)
user, err = GetMaskedUser(user, false)
if err != nil {
return nil, err
}
@@ -832,11 +837,14 @@ func userChangeTrigger(oldName string, newName string) error {
}
func (user *User) IsMfaEnabled() bool {
if user == nil {
return false
}
return user.PreferredMfaType != ""
}
func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
if user.PreferredMfaType == "" {
if user == nil || user.PreferredMfaType == "" {
return nil
}
return user.GetMfaProps(user.PreferredMfaType, masked)

View File

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

View File

@@ -156,7 +156,7 @@ func AuthzFilter(ctx *context.Context) {
urlPath := getUrlPath(ctx.Request.URL.Path)
objOwner, objName := "", ""
if urlPath != "/api/get-app-login" {
if urlPath != "/api/get-app-login" && urlPath != "/api/get-resource" {
objOwner, objName = getObject(ctx)
}

View File

@@ -33,6 +33,13 @@ func CorsFilter(ctx *context.Context) {
origin := ctx.Input.Header(headerOrigin)
originConf := conf.GetConfigString("origin")
if ctx.Request.Method == "POST" && ctx.Request.RequestURI == "/api/login/oauth/access_token" {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
return
}
if origin != "" && originConf != "" && origin != originConf {
ok, err := object.IsOriginAllowed(origin)
if err != nil {

View File

@@ -23,7 +23,7 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
case "AWS S3":
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "MinIO":
return NewMinIOS3StorageProvider(clientId, clientSecret, region, bucket, endpoint)
return NewMinIOS3StorageProvider(clientId, clientSecret, "_", bucket, endpoint)
case "Aliyun OSS":
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "Tencent Cloud COS":

View File

@@ -188,7 +188,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.AddLdap"
"description": "add ldap",
"operationId": "ApiController.AddLdap",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the ldap",
"required": true,
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/add-message": {
@@ -476,7 +496,26 @@
"tags": [
"Resource API"
],
"operationId": "ApiController.AddResource"
"operationId": "ApiController.AddResource",
"parameters": [
{
"in": "body",
"name": "resource",
"description": "Resource object",
"required": true,
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
],
"responses": {
"200": {
"description": "Success or error",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/add-role": {
@@ -1067,7 +1106,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.DeleteLdap"
"description": "delete ldap",
"operationId": "ApiController.DeleteLdap",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the ldap",
"required": true,
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/delete-message": {
@@ -1344,7 +1403,26 @@
"tags": [
"Resource API"
],
"operationId": "ApiController.DeleteResource"
"operationId": "ApiController.DeleteResource",
"parameters": [
{
"in": "body",
"name": "resource",
"description": "Resource object",
"required": true,
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
],
"responses": {
"200": {
"description": "Success or error",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/delete-role": {
@@ -2060,7 +2138,25 @@
"tags": [
"Account API"
],
"operationId": "ApiController.GetLdap"
"description": "get ldap",
"operationId": "ApiController.GetLdap",
"parameters": [
{
"in": "query",
"name": "id",
"description": "id",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
}
}
},
"/api/get-ldap-users": {
@@ -2068,7 +2164,16 @@
"tags": [
"Account API"
],
"operationId": "ApiController.GetLdapser"
"description": "get ldap users",
"operationId": "ApiController.GetLdapser",
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/LdapResp"
}
}
}
}
},
"/api/get-ldaps": {
@@ -2076,7 +2181,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.GetLdaps"
"description": "get ldaps",
"operationId": "ApiController.GetLdaps",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "owner",
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Ldap"
}
}
}
}
}
},
"/api/get-message": {
@@ -2797,7 +2922,25 @@
"tags": [
"Resource API"
],
"operationId": "ApiController.GetResource"
"description": "get resource",
"operationId": "ApiController.GetResource",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of resource",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
}
}
},
"/api/get-resources": {
@@ -2805,7 +2948,71 @@
"tags": [
"Resource API"
],
"operationId": "ApiController.GetResources"
"description": "get resources",
"operationId": "ApiController.GetResources",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "Owner",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "user",
"description": "User",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "pageSize",
"description": "Page Size",
"type": "integer"
},
{
"in": "query",
"name": "p",
"description": "Page Number",
"type": "integer"
},
{
"in": "query",
"name": "field",
"description": "Field",
"type": "string"
},
{
"in": "query",
"name": "value",
"description": "Value",
"type": "string"
},
{
"in": "query",
"name": "sortField",
"description": "Sort Field",
"type": "string"
},
{
"in": "query",
"name": "sortOrder",
"description": "Sort Order",
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Resource"
}
}
}
}
}
},
"/api/get-role": {
@@ -4019,7 +4226,25 @@
"tags": [
"Account API"
],
"operationId": "ApiController.SyncLdapUsers"
"description": "sync ldap users",
"operationId": "ApiController.SyncLdapUsers",
"parameters": [
{
"in": "query",
"name": "id",
"description": "id",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/LdapSyncResp"
}
}
}
}
},
"/api/unlink": {
@@ -4209,7 +4434,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.UpdateLdap"
"description": "update ldap",
"operationId": "ApiController.UpdateLdap",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the ldap",
"required": true,
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/update-message": {
@@ -4532,7 +4777,34 @@
"tags": [
"Resource API"
],
"operationId": "ApiController.UpdateResource"
"description": "get resource",
"operationId": "ApiController.UpdateResource",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of resource",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "resource",
"description": "The resource object",
"required": true,
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
],
"responses": {
"200": {
"description": "Success or error",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/update-role": {
@@ -4779,7 +5051,76 @@
"tags": [
"Resource API"
],
"operationId": "ApiController.UploadResource"
"operationId": "ApiController.UploadResource",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "Owner",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "user",
"description": "User",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "application",
"description": "Application",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "tag",
"description": "Tag",
"type": "string"
},
{
"in": "query",
"name": "parent",
"description": "Parent",
"type": "string"
},
{
"in": "query",
"name": "fullFilePath",
"description": "Full File Path",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "createdTime",
"description": "Created Time",
"type": "string"
},
{
"in": "query",
"name": "description",
"description": "Description",
"type": "string"
},
{
"in": "formData",
"name": "file",
"description": "Resource file",
"required": true,
"type": "file"
}
],
"responses": {
"200": {
"description": "FileUrl, objectKey",
"schema": {
"$ref": "#/definitions/object.Resource"
}
}
}
}
},
"/api/user": {
@@ -4936,6 +5277,14 @@
"title": "LaravelResponse",
"type": "object"
},
"LdapResp": {
"title": "LdapResp",
"type": "object"
},
"LdapSyncResp": {
"title": "LdapSyncResp",
"type": "object"
},
"Response": {
"title": "Response",
"type": "object"
@@ -4974,13 +5323,13 @@
"properties": {
"data": {
"additionalProperties": {
"description": "support string | class | List\u003cclass\u003e and os on",
"description": "support string, struct or []struct",
"type": "string"
}
},
"data2": {
"additionalProperties": {
"description": "support string | class | List\u003cclass\u003e and os on",
"description": "support string, struct or []struct",
"type": "string"
}
},
@@ -5196,6 +5545,12 @@
"signupUrl": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"termsOfUse": {
"type": "string"
},
@@ -5459,6 +5814,59 @@
}
}
},
"object.Ldap": {
"title": "Ldap",
"type": "object",
"properties": {
"autoSync": {
"type": "integer",
"format": "int64"
},
"baseDn": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"enableSsl": {
"type": "boolean"
},
"filter": {
"type": "string"
},
"filterFields": {
"type": "array",
"items": {
"type": "string"
}
},
"host": {
"type": "string"
},
"id": {
"type": "string"
},
"lastSync": {
"type": "string"
},
"owner": {
"type": "string"
},
"password": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int64"
},
"serverName": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"object.ManagedAccount": {
"title": "ManagedAccount",
"type": "object",
@@ -6126,9 +6534,6 @@
"customLogo": {
"type": "string"
},
"scopes": {
"type": "string"
},
"customTokenUrl": {
"type": "string"
},
@@ -6190,6 +6595,9 @@
"regionId": {
"type": "string"
},
"scopes": {
"type": "string"
},
"signName": {
"type": "string"
},
@@ -6204,6 +6612,11 @@
},
"type": {
"type": "string"
},
"userMapping": {
"additionalProperties": {
"type": "string"
}
}
}
},
@@ -6286,6 +6699,55 @@
}
}
},
"object.Resource": {
"title": "Resource",
"type": "object",
"properties": {
"application": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"description": {
"type": "string"
},
"fileFormat": {
"type": "string"
},
"fileName": {
"type": "string"
},
"fileSize": {
"type": "integer",
"format": "int64"
},
"fileType": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"parent": {
"type": "string"
},
"provider": {
"type": "string"
},
"tag": {
"type": "string"
},
"url": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"object.Role": {
"title": "Role",
"type": "object",

View File

@@ -122,7 +122,20 @@ paths:
post:
tags:
- Account API
description: add ldap
operationId: ApiController.AddLdap
parameters:
- in: body
name: body
description: The details of the ldap
required: true
schema:
$ref: '#/definitions/object.Ldap'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-message:
post:
tags:
@@ -308,6 +321,18 @@ paths:
tags:
- Resource API
operationId: ApiController.AddResource
parameters:
- in: body
name: resource
description: Resource object
required: true
schema:
$ref: '#/definitions/object.Resource'
responses:
"200":
description: Success or error
schema:
$ref: '#/definitions/controllers.Response'
/api/add-role:
post:
tags:
@@ -690,7 +715,20 @@ paths:
post:
tags:
- Account API
description: delete ldap
operationId: ApiController.DeleteLdap
parameters:
- in: body
name: body
description: The details of the ldap
required: true
schema:
$ref: '#/definitions/object.Ldap'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-message:
post:
tags:
@@ -869,6 +907,18 @@ paths:
tags:
- Resource API
operationId: ApiController.DeleteResource
parameters:
- in: body
name: resource
description: Resource object
required: true
schema:
$ref: '#/definitions/object.Resource'
responses:
"200":
description: Success or error
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-role:
post:
tags:
@@ -1336,17 +1386,48 @@ paths:
get:
tags:
- Account API
description: get ldap
operationId: ApiController.GetLdap
parameters:
- in: query
name: id
description: id
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Ldap'
/api/get-ldap-users:
get:
tags:
- Account API
description: get ldap users
operationId: ApiController.GetLdapser
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/LdapResp'
/api/get-ldaps:
get:
tags:
- Account API
description: get ldaps
operationId: ApiController.GetLdaps
parameters:
- in: query
name: owner
description: owner
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Ldap'
/api/get-message:
get:
tags:
@@ -1818,12 +1899,67 @@ paths:
get:
tags:
- Resource API
description: get resource
operationId: ApiController.GetResource
parameters:
- in: query
name: id
description: The id ( owner/name ) of resource
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Resource'
/api/get-resources:
get:
tags:
- Resource API
description: get resources
operationId: ApiController.GetResources
parameters:
- in: query
name: owner
description: Owner
required: true
type: string
- in: query
name: user
description: User
required: true
type: string
- in: query
name: pageSize
description: Page Size
type: integer
- in: query
name: p
description: Page Number
type: integer
- in: query
name: field
description: Field
type: string
- in: query
name: value
description: Value
type: string
- in: query
name: sortField
description: Sort Field
type: string
- in: query
name: sortOrder
description: Sort Order
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Resource'
/api/get-role:
get:
tags:
@@ -2624,7 +2760,19 @@ paths:
post:
tags:
- Account API
description: sync ldap users
operationId: ApiController.SyncLdapUsers
parameters:
- in: query
name: id
description: id
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/LdapSyncResp'
/api/unlink:
post:
tags:
@@ -2748,7 +2896,20 @@ paths:
post:
tags:
- Account API
description: update ldap
operationId: ApiController.UpdateLdap
parameters:
- in: body
name: body
description: The details of the ldap
required: true
schema:
$ref: '#/definitions/object.Ldap'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-message:
post:
tags:
@@ -2960,7 +3121,25 @@ paths:
post:
tags:
- Resource API
description: get resource
operationId: ApiController.UpdateResource
parameters:
- in: query
name: id
description: The id ( owner/name ) of resource
required: true
type: string
- in: body
name: resource
description: The resource object
required: true
schema:
$ref: '#/definitions/object.Resource'
responses:
"200":
description: Success or error
schema:
$ref: '#/definitions/controllers.Response'
/api/update-role:
post:
tags:
@@ -3123,6 +3302,53 @@ paths:
tags:
- Resource API
operationId: ApiController.UploadResource
parameters:
- in: query
name: owner
description: Owner
required: true
type: string
- in: query
name: user
description: User
required: true
type: string
- in: query
name: application
description: Application
required: true
type: string
- in: query
name: tag
description: Tag
type: string
- in: query
name: parent
description: Parent
type: string
- in: query
name: fullFilePath
description: Full File Path
required: true
type: string
- in: query
name: createdTime
description: Created Time
type: string
- in: query
name: description
description: Description
type: string
- in: formData
name: file
description: Resource file
required: true
type: file
responses:
"200":
description: FileUrl, objectKey
schema:
$ref: '#/definitions/object.Resource'
/api/user:
get:
tags:
@@ -3223,6 +3449,12 @@ definitions:
LaravelResponse:
title: LaravelResponse
type: object
LdapResp:
title: LdapResp
type: object
LdapSyncResp:
title: LdapSyncResp
type: object
Response:
title: Response
type: object
@@ -3251,11 +3483,11 @@ definitions:
properties:
data:
additionalProperties:
description: support string | class | List<class> and os on
description: support string, struct or []struct
type: string
data2:
additionalProperties:
description: support string | class | List<class> and os on
description: support string, struct or []struct
type: string
msg:
type: string
@@ -3400,6 +3632,10 @@ definitions:
$ref: '#/definitions/object.SignupItem'
signupUrl:
type: string
tags:
type: array
items:
type: string
termsOfUse:
type: string
themeData:
@@ -3577,6 +3813,42 @@ definitions:
type: string
username:
type: string
object.Ldap:
title: Ldap
type: object
properties:
autoSync:
type: integer
format: int64
baseDn:
type: string
createdTime:
type: string
enableSsl:
type: boolean
filter:
type: string
filterFields:
type: array
items:
type: string
host:
type: string
id:
type: string
lastSync:
type: string
owner:
type: string
password:
type: string
port:
type: integer
format: int64
serverName:
type: string
username:
type: string
object.ManagedAccount:
title: ManagedAccount
type: object
@@ -4026,8 +4298,6 @@ definitions:
type: string
customLogo:
type: string
scopes:
type: string
customTokenUrl:
type: string
customUserInfoUrl:
@@ -4069,6 +4339,8 @@ definitions:
type: string
regionId:
type: string
scopes:
type: string
signName:
type: string
subType:
@@ -4079,6 +4351,9 @@ definitions:
type: string
type:
type: string
userMapping:
additionalProperties:
type: string
object.ProviderItem:
title: ProviderItem
type: object
@@ -4132,6 +4407,39 @@ definitions:
type: string
user:
type: string
object.Resource:
title: Resource
type: object
properties:
application:
type: string
createdTime:
type: string
description:
type: string
fileFormat:
type: string
fileName:
type: string
fileSize:
type: integer
format: int64
fileType:
type: string
name:
type: string
owner:
type: string
parent:
type: string
provider:
type: string
tag:
type: string
url:
type: string
user:
type: string
object.Role:
title: Role
type: object

View File

@@ -50,4 +50,32 @@ module.exports = {
},
},
],
webpack: {
// use polyfill Buffer with Webpack 5
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
// https://craco.js.org/docs/configuration/webpack/
configure: (webpackConfig, { env, paths }) => {
webpackConfig.resolve.fallback = {
// "process": require.resolve('process/browser'),
// "util": require.resolve("util/"),
// "url": require.resolve("url/"),
// "zlib": require.resolve("browserify-zlib"),
// "stream": require.resolve("stream-browserify"),
// "http": require.resolve("stream-http"),
// "https": require.resolve("https-browserify"),
// "assert": require.resolve("assert/"),
"buffer": require.resolve('buffer/'),
"process": false,
"util": false,
"url": false,
"zlib": false,
"stream": false,
"http": false,
"https": false,
"assert": false,
"buffer": false,
};
return webpackConfig;
},
}
};

View File

@@ -9,11 +9,13 @@
"@crowdin/cli": "^3.7.10",
"@ctrl/tinycolor": "^3.5.0",
"@emotion/react": "^11.10.5",
"@metamask/eth-sig-util": "^6.0.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"antd": "5.2.3",
"antd-token-previewer": "^1.1.0-22",
"buffer": "^6.0.3",
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0",
@@ -34,6 +36,7 @@
"react-helmet": "^6.1.0",
"react-highlight-words": "^0.18.0",
"react-i18next": "^11.8.7",
"react-metamask-avatar": "^1.2.1",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.1",
"react-social-login-buttons": "^3.4.0"

View File

@@ -68,7 +68,7 @@ class AdapterEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
@@ -80,8 +80,9 @@ class AdapterEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
models: res,
models: res.data,
});
});
}

View File

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

View File

@@ -119,7 +119,7 @@ class ApplicationEditPage extends React.Component {
getApplication() {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -138,7 +138,7 @@ class ApplicationEditPage extends React.Component {
}
this.setState({
application: res,
application: res.data,
});
this.getCerts(res.organization);
@@ -184,9 +184,9 @@ class ApplicationEditPage extends React.Component {
getSamlMetadata() {
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName)
.then((res) => {
.then((data) => {
this.setState({
samlMetadata: res,
samlMetadata: data,
});
});
}

View File

@@ -45,7 +45,7 @@ class CertEditPage extends React.Component {
getCert() {
CertBackend.getCert(this.state.owner, this.state.certName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -56,7 +56,7 @@ class CertEditPage extends React.Component {
}
this.setState({
cert: res,
cert: res.data,
});
});
}
@@ -65,7 +65,7 @@ class CertEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
@@ -158,6 +158,7 @@ class CertEditPage extends React.Component {
{
[
{id: "x509", name: "x509"},
{id: "Payment", name: "Payment"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>

View File

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

View File

@@ -41,7 +41,7 @@ class ChatEditPage extends React.Component {
getChat() {
ChatBackend.getChat("admin", this.state.chatName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -51,7 +51,7 @@ class ChatEditPage extends React.Component {
return;
}
this.setState({
chat: res,
chat: res.data,
});
this.getUsers(res.organization);
@@ -62,7 +62,7 @@ class ChatEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
@@ -76,7 +76,7 @@ class ChatEditPage extends React.Component {
}
this.setState({
users: res,
users: res.data,
});
});
}

View File

@@ -79,7 +79,8 @@ class ChatPage extends BaseListPage {
getMessages(chatName) {
MessageBackend.getChatMessages(chatName)
.then((messages) => {
.then((res) => {
const messages = res.data;
this.setState({
messages: messages,
});
@@ -229,7 +230,7 @@ class ChatPage extends BaseListPage {
</div>
)
}
<ChatBox messages={this.state.messages} sendMessage={(text) => {this.sendMessage(text);}} account={this.props.account} />
<ChatBox messages={this.state.messages || []} sendMessage={(text) => {this.sendMessage(text);}} account={this.props.account} />
</div>
</div>
);

View File

@@ -41,7 +41,7 @@ class EntryPage extends React.Component {
renderHomeIfLoggedIn(component) {
if (this.props.account !== null && this.props.account !== undefined) {
return <Redirect to="/" />;
return <Redirect to={{pathname: "/", state: {from: "/login"}}} />;
} else {
return component;
}
@@ -79,7 +79,9 @@ class EntryPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
const themeData = res !== null ? Setting.getThemeData(res.organizationObj, res) : Conf.ThemeDefault;
const application = res.data;
const themeData = application !== null ? Setting.getThemeData(application.organizationObj, application) : Conf.ThemeDefault;
this.props.updataThemeData(themeData);
});
};

View File

@@ -67,7 +67,7 @@ class GroupEditPage extends React.Component {
.then((res) => {
if (res.status === "ok") {
this.setState({
organizations: res.data,
organizations: res.data || [],
});
}
});

View File

@@ -55,7 +55,7 @@ class LdapEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}

View File

@@ -46,7 +46,7 @@ class MessageEditPage extends React.Component {
getMessage() {
MessageBackend.getMessage("admin", this.state.messageName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -54,10 +54,10 @@ class MessageEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
message: res,
});
this.setState({
message: res.data,
});
this.getUsers(res.organization);
});
}
@@ -66,7 +66,7 @@ class MessageEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
@@ -87,8 +87,9 @@ class MessageEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
users: res.data,
});
});
}

View File

@@ -48,7 +48,7 @@ class ModelEditPage extends React.Component {
getModel() {
ModelBackend.getModel(this.state.organizationName, this.state.modelName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -59,7 +59,7 @@ class ModelEditPage extends React.Component {
}
this.setState({
model: res,
model: res.data,
});
});
}
@@ -68,7 +68,7 @@ class ModelEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}

View File

@@ -75,7 +75,7 @@ class OrganizationEditPage extends React.Component {
}
this.setState({
applications: res,
applications: res.data || [],
});
});
}

View File

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

View File

@@ -41,14 +41,14 @@ class PaymentEditPage extends React.Component {
getPayment() {
PaymentBackend.getPayment("admin", this.state.paymentName)
.then((payment) => {
if (payment === null) {
.then((res) => {
if (res.data === null) {
this.props.history.push("/404");
return;
}
this.setState({
payment: payment,
payment: res.data,
});
Setting.scrollToDiv("invoice-area");

View File

@@ -50,7 +50,9 @@ class PermissionEditPage extends React.Component {
getPermission() {
PermissionBackend.getPermission(this.state.organizationName, this.state.permissionName)
.then((res) => {
if (res === null) {
const permission = res.data;
if (permission === null) {
this.props.history.push("/404");
return;
}
@@ -61,14 +63,14 @@ class PermissionEditPage extends React.Component {
}
this.setState({
permission: res,
permission: permission,
});
this.getUsers(res.owner);
this.getRoles(res.owner);
this.getModels(res.owner);
this.getResources(res.owner);
this.getModel(res.owner, res.model);
this.getUsers(permission.owner);
this.getRoles(permission.owner);
this.getModels(permission.owner);
this.getResources(permission.owner);
this.getModel(permission.owner, permission.model);
});
}
@@ -76,7 +78,7 @@ class PermissionEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
@@ -88,8 +90,9 @@ class PermissionEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
users: res.data,
});
});
}
@@ -101,8 +104,9 @@ class PermissionEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res,
roles: res.data,
});
});
}
@@ -114,21 +118,21 @@ class PermissionEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
models: res,
models: res.data,
});
});
}
getModel(organizationName, modelName) {
if (modelName === "") {
return;
}
ModelBackend.getModel(organizationName, modelName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
model: res,
model: res.data,
});
});
}
@@ -137,7 +141,7 @@ class PermissionEditPage extends React.Component {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
resources: (res.msg === undefined) ? res : [],
resources: res.data || [],
});
});
}

View File

@@ -68,8 +68,9 @@ class PlanEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res,
roles: res.data,
});
});
}
@@ -81,8 +82,9 @@ class PlanEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
users: res.data,
});
});
}
@@ -91,7 +93,7 @@ class PlanEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}

View File

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

View File

@@ -44,13 +44,12 @@ class PricingEditPage extends React.Component {
this.getPricing();
this.getOrganizations();
this.getApplicationsByOrganization(this.state.organizationName);
this.getUserApplication();
}
getPricing() {
PricingBackend.getPricing(this.state.organizationName, this.state.pricingName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -61,7 +60,7 @@ class PricingEditPage extends React.Component {
}
this.setState({
pricing: res,
pricing: res.data,
});
this.getPlans(res.owner);
});
@@ -74,8 +73,9 @@ class PricingEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
plans: res,
plans: res.data,
});
});
}
@@ -84,7 +84,16 @@ class PricingEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
getApplicationsByOrganization(organizationName) {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
applications: res.data || [],
});
});
}
@@ -107,28 +116,6 @@ class PricingEditPage extends React.Component {
});
}
getApplicationsByOrganization(organizationName) {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
applications: (res.msg === undefined) ? res : [],
});
});
}
getUserApplication() {
ApplicationBackend.getUserApplication(this.state.organizationName, this.state.userName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
application: res,
});
});
}
renderPricing() {
return (
<Card size="small" title={

View File

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

View File

@@ -48,7 +48,7 @@ class ProductBuyPage extends React.Component {
}
this.setState({
product: res,
product: res.data,
});
});
}

View File

@@ -62,7 +62,7 @@ class ProductEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}

View File

@@ -50,7 +50,7 @@ class ProviderEditPage extends React.Component {
getProvider() {
ProviderBackend.getProvider(this.state.owner, this.state.providerName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -72,7 +72,7 @@ class ProviderEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: res.msg === undefined ? res : [],
organizations: res.data || [],
});
});
}
@@ -236,7 +236,10 @@ class ProviderEditPage extends React.Component {
tooltip = i18next.t("provider:Agent ID - Tooltip");
}
} else if (provider.category === "SMS") {
if (provider.type === "Tencent Cloud SMS") {
if (provider.type === "Twilio SMS") {
text = i18next.t("provider:Sender number");
tooltip = i18next.t("provider:Sender number - Tooltip");
} else if (provider.type === "Tencent Cloud SMS") {
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
} else if (provider.type === "Volc Engine SMS") {
@@ -355,6 +358,8 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Default");
} else if (value === "AI") {
this.updateProviderField("type", "OpenAI API - GPT");
} else if (value === "Web3") {
this.updateProviderField("type", "MetaMask");
}
})}>
{
@@ -367,6 +372,7 @@ class ProviderEditPage extends React.Component {
{id: "SAML", name: "SAML"},
{id: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"},
{id: "Web3", name: "Web3"},
]
.sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
@@ -521,7 +527,7 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.category === "Captcha" && this.state.provider.type === "Default" ? null : (
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") || (this.state.provider.category === "Web3") || (this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ? null : (
<React.Fragment>
{
this.state.provider.category === "AI" ? null : (
@@ -610,36 +616,42 @@ class ProviderEditPage extends React.Component {
}
{this.state.provider.category === "Storage" ? (
<div>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.endpoint} onChange={e => {
this.updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.intranetEndpoint} onChange={e => {
this.updateProviderField("intranetEndpoint", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.bucket} onChange={e => {
this.updateProviderField("bucket", e.target.value);
}} />
</Col>
</Row>
{["Local File System"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.endpoint} onChange={e => {
this.updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>
)}
{["Local File System", "MinIO"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.intranetEndpoint} onChange={e => {
this.updateProviderField("intranetEndpoint", e.target.value);
}} />
</Col>
</Row>
)}
{["Local File System"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.bucket} onChange={e => {
this.updateProviderField("bucket", e.target.value);
}} />
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
@@ -650,17 +662,19 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.domain} onChange={e => {
this.updateProviderField("domain", e.target.value);
}} />
</Col>
</Row>
{["AWS S3", "MinIO", "Tencent Cloud COS"].includes(this.state.provider.type) ? (
{["MinIO"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.domain} disabled={this.state.provider.type === "Local File System"} onChange={e => {
this.updateProviderField("domain", e.target.value);
}} />
</Col>
</Row>
)}
{["AWS S3", "Tencent Cloud COS"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} :
@@ -674,6 +688,7 @@ class ProviderEditPage extends React.Component {
) : null}
</div>
) : null}
{this.getAppIdRow(this.state.provider)}
{
this.state.provider.category === "Email" ? (
<React.Fragment>
@@ -919,7 +934,6 @@ class ProviderEditPage extends React.Component {
</Row>
) : null
}
{this.getAppIdRow(this.state.provider)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :

View File

@@ -43,7 +43,7 @@ class RoleEditPage extends React.Component {
getRole() {
RoleBackend.getRole(this.state.organizationName, this.state.roleName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -53,7 +53,7 @@ class RoleEditPage extends React.Component {
}
this.setState({
role: res,
role: res.data,
});
this.getUsers(res.owner);
@@ -65,7 +65,7 @@ class RoleEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
@@ -77,8 +77,9 @@ class RoleEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
users: res.data,
});
});
}
@@ -90,8 +91,9 @@ class RoleEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res,
roles: res.data,
});
});
}

View File

@@ -34,10 +34,10 @@ export const ServerUrl = "";
export const StaticBaseUrl = "https://cdn.casbin.org";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "中文", key: "zh", country: "CN", alt: "中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "中文", key: "zh", country: "CN", alt: "中文"},
{label: "Indonesia", key: "id", country: "ID", alt: "Indonesia"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
@@ -216,6 +216,12 @@ export const OtherProviderInfo = {
url: "https://platform.openai.com",
},
},
Web3: {
"MetaMask": {
logo: `${StaticBaseUrl}/img/social_metamask.svg`,
url: "https://metamask.io/",
},
},
};
export function initCountries() {
@@ -288,7 +294,7 @@ export function isProviderVisible(providerItem) {
return false;
}
if (providerItem.provider.category !== "OAuth" && providerItem.provider.category !== "SAML") {
if (!["OAuth", "SAML", "Web3"].includes(providerItem.provider.category)) {
return false;
}
@@ -482,6 +488,26 @@ export function isPromptAnswered(user, application) {
return true;
}
export const MfaRuleRequired = "Required";
export const MfaRulePrompted = "Prompted";
export const MfaRuleOptional = "Optional";
export function isRequiredEnableMfa(user, organization) {
if (!user || !organization || !organization.mfaItems) {
return false;
}
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
}
export function getMfaItemsByRules(user, organization, mfaRules = []) {
if (!user || !organization || !organization.mfaItems) {
return [];
}
return organization.mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
.filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
}
export function parseObject(s) {
try {
return eval("(" + s + ")");
@@ -871,6 +897,10 @@ export function getProviderTypeOptions(category) {
return ([
{id: "OpenAI API - GPT", name: "OpenAI API - GPT"},
]);
} else if (category === "Web3") {
return ([
{id: "MetaMask", name: "MetaMask"},
]);
} else {
return [];
}

View File

@@ -47,7 +47,7 @@ class SubscriptionEditPage extends React.Component {
getSubscription() {
SubscriptionBackend.getSubscription(this.state.organizationName, this.state.subscriptionName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -58,7 +58,7 @@ class SubscriptionEditPage extends React.Component {
}
this.setState({
subscription: res,
subscription: res.data,
});
this.getUsers(res.owner);
@@ -70,7 +70,7 @@ class SubscriptionEditPage extends React.Component {
PlanBackend.getPlans(organizationName)
.then((res) => {
this.setState({
planes: res,
planes: res.data,
});
});
}
@@ -82,8 +82,9 @@ class SubscriptionEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res,
users: res.data,
});
});
}
@@ -92,7 +93,7 @@ class SubscriptionEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}

View File

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

View File

@@ -48,7 +48,7 @@ class SyncerEditPage extends React.Component {
getSyncer() {
SyncerBackend.getSyncer("admin", this.state.syncerName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -59,7 +59,7 @@ class SyncerEditPage extends React.Component {
}
this.setState({
syncer: res,
syncer: res.data,
});
});
}
@@ -68,7 +68,7 @@ class SyncerEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}

View File

@@ -36,7 +36,7 @@ class TokenEditPage extends React.Component {
getToken() {
TokenBackend.getToken("admin", this.state.tokenName)
.then((res) => {
if (res === null) {
if (res.data === null) {
this.props.history.push("/404");
return;
}
@@ -47,7 +47,7 @@ class TokenEditPage extends React.Component {
}
this.setState({
token: res,
token: res.data,
});
});
}

View File

@@ -14,6 +14,7 @@
import React from "react";
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd";
import {withRouter} from "react-router-dom";
import * as GroupBackend from "./backend/GroupBackend";
import * as UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
@@ -35,6 +36,7 @@ import PopconfirmModal from "./common/modal/PopconfirmModal";
import {DeleteMfa} from "./backend/MfaBackend";
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as MfaBackend from "./backend/MfaBackend";
import AccountAvatar from "./account/AccountAvatar";
const {Option} = Select;
@@ -53,6 +55,7 @@ class UserEditPage extends React.Component {
mode: props.location.mode !== undefined ? props.location.mode : "edit",
loading: true,
returnUrl: null,
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
};
}
@@ -72,19 +75,20 @@ class UserEditPage extends React.Component {
getUser() {
UserBackend.getUser(this.state.organizationName, this.state.userName)
.then((data) => {
if (data === null) {
.then((res) => {
if (res.data === null) {
this.props.history.push("/404");
return;
}
if (data.status === null || data.status !== "error") {
this.setState({
user: data,
multiFactorAuths: data?.multiFactorAuths ?? [],
});
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
user: res.data,
multiFactorAuths: res.data?.multiFactorAuths ?? [],
loading: false,
});
});
@@ -105,7 +109,7 @@ class UserEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}
@@ -114,7 +118,7 @@ class UserEditPage extends React.Component {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
applications: (res.msg === undefined) ? res : [],
applications: res.data || [],
});
});
}
@@ -126,12 +130,10 @@ class UserEditPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
application: res,
});
this.setState({
isGroupsVisible: res.organizationObj.accountItems?.some((item) => item.name === "Groups" && item.visible),
application: res.data,
isGroupsVisible: res.data?.organizationObj.accountItems?.some((item) => item.name === "Groups" && item.visible),
});
});
}
@@ -269,6 +271,12 @@ class UserEditPage extends React.Component {
}
}
if (accountItem.name === "ID card info" || accountItem.name === "ID card") {
if (this.state.user.properties?.isIdCardVerified === "true") {
disabled = true;
}
}
let isKeysGenerated = false;
if (this.state.user.accessKey !== "" && this.state.user.accessKey !== "") {
isKeysGenerated = true;
@@ -365,20 +373,11 @@ class UserEditPage extends React.Component {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Preview")}:
</Col>
<Col span={22} >
<a target="_blank" rel="noreferrer" href={this.state.user.avatar}>
<img src={this.state.user.avatar} alt={this.state.user.avatar} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<CropperDivModal buttonText={`${i18next.t("user:Upload a photo")}...`} title={i18next.t("user:Upload a photo")} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />
</Row>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Preview")}:
</Col>
<Col>
{this.renderImage(this.state.user.avatar, i18next.t("user:Upload a photo"), i18next.t("user:Set new profile picture"), "avatar", false)}
</Col>
</Row>
);
@@ -536,12 +535,36 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:ID card"), i18next.t("user:ID card - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.idCard} onChange={e => {
<Input value={this.state.user.idCard} disabled={disabled} onChange={e => {
this.updateUserField("idCard", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "ID card info") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:ID card info"), i18next.t("user:ID card info - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Preview")}:
</Col>
{
[
{name: "ID card front", value: "idCardFront"},
{name: "ID card back", value: "idCardBack"},
{name: "ID card with person", value: "idCardWithPerson"},
].map((entry) => {
return this.renderImage(this.state.user.properties[entry.value] || "", this.getIdCardType(entry.name), this.getIdCardText(entry.name), entry.value, disabled);
})
}
</Row>
</Col>
</Row>
);
} else if (accountItem.name === "Homepage") {
return (
<Row style={{marginTop: "20px"}} >
@@ -768,10 +791,23 @@ class UserEditPage extends React.Component {
{
(this.state.application === null || this.state.user === null) ? null : (
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem) =>
(providerItem.provider.category === "OAuth") ? (
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />
(providerItem.provider.category === "OAuth" || providerItem.provider.category === "Web3") ? (
<OAuthWidget
key={providerItem.name}
labelSpan={(Setting.isMobile()) ? 10 : 3}
user={this.state.user}
application={this.state.application}
providerItem={providerItem}
account={this.props.account}
onUnlinked={() => {return this.unlinked();}} />
) : (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />
<SamlWidget
key={providerItem.name}
labelSpan={(Setting.isMobile()) ? 10 : 3}
user={this.state.user}
application={this.state.application}
providerItem={providerItem}
onUnlinked={() => {return this.unlinked();}} />
)
)
)
@@ -900,7 +936,7 @@ class UserEditPage extends React.Component {
}
</Space>
) : <Button type={"default"} onClick={() => {
Setting.goToLink(`/mfa-authentication/setup?mfaType=${item.mfaType}`);
this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
}}>
{i18next.t("mfa:Setup")}
</Button>}
@@ -942,6 +978,25 @@ class UserEditPage extends React.Component {
}
}
renderImage(imgUrl, title, set, tag, disabled) {
return (
<Col span={4} style={{textAlign: "center", margin: "auto"}} key={tag}>
{
imgUrl ?
<a target="_blank" rel="noreferrer" href={imgUrl} style={{marginBottom: "10px"}}>
<AccountAvatar src={imgUrl} alt={imgUrl} size={90} style={{marginBottom: "20px"}} />
</a>
:
<Col style={{height: "78%", border: "1px dotted grey", borderRadius: 3, marginBottom: 5}}>
<div style={{fontSize: 30, margin: 10}}>+</div>
<div style={{verticalAlign: "middle", marginBottom: 10}}>{`Upload ${title}...`}</div>
</Col>
}
<CropperDivModal disabled={disabled} tag={tag} setTitle={set} buttonText={`${title}...`} title={title} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />
</Col>
);
}
renderUser() {
return (
<Card size="small" title={
@@ -967,6 +1022,30 @@ class UserEditPage extends React.Component {
);
}
getIdCardType(key) {
if (key === "ID card front") {
return i18next.t("user:ID card front");
} else if (key === "ID card back") {
return i18next.t("user:ID card back");
} else if (key === "ID card with person") {
return i18next.t("user:ID card with person");
} else {
return "Unknown Id card name: " + key;
}
}
getIdCardText(key) {
if (key === "ID card front") {
return i18next.t("user:Upload ID card front picture");
} else if (key === "ID card back") {
return i18next.t("user:Upload ID card back picture");
} else if (key === "ID card with person") {
return i18next.t("user:Upload ID card with person picture");
} else {
return "Unknown Id card name: " + key;
}
}
submitUserEdit(needExit) {
const user = Setting.deepCopy(this.state.user);
UserBackend.updateUser(this.state.organizationName, this.state.userName, user)
@@ -1053,4 +1132,4 @@ class UserEditPage extends React.Component {
}
}
export default UserEditPage;
export default withRouter(UserEditPage);

View File

@@ -23,6 +23,7 @@ import * as UserBackend from "./backend/UserBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import AccountAvatar from "./account/AccountAvatar";
class UserListPage extends BaseListPage {
constructor(props) {
@@ -270,7 +271,7 @@ class UserListPage extends BaseListPage {
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
<img referrerPolicy="no-referrer" src={text} alt={text} width={50} />
<AccountAvatar referrerPolicy="no-referrer" src={text} alt={text} size={50} />
</a>
);
},

View File

@@ -122,14 +122,14 @@ class WebhookEditPage extends React.Component {
getWebhook() {
WebhookBackend.getWebhook("admin", this.state.webhookName)
.then((webhook) => {
if (webhook === null) {
.then((res) => {
if (res.data === null) {
this.props.history.push("/404");
return;
}
this.setState({
webhook: webhook,
webhook: res.data,
});
});
}
@@ -138,7 +138,7 @@ class WebhookEditPage extends React.Component {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
organizations: res.data || [],
});
});
}

View File

@@ -0,0 +1,36 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {MetaMaskAvatar} from "react-metamask-avatar";
class AccountAvatar extends React.Component {
render() {
const {src, size} = this.props;
// The avatar for Metamask account is directly generated by an algorithm based on the address
// src = "metamask:0xC304b2cC0Be8E9ce10fF3Afd34820Ed306A23600";
const matchMetaMask = src.match(/^metamask:(\w+)$/);
if (matchMetaMask) {
const address = matchMetaMask[1];
return (
<MetaMaskAvatar address={address} size={size} />
);
}
return (
<img width={size} height={size} src={src} />
);
}
}
export default AccountAvatar;

View File

@@ -95,6 +95,12 @@ class AuthCallback extends React.Component {
if (code === null) {
code = params.get("authCode");
}
// The code for Metamask is the JSON-serialized string of Web3AuthToken
// Due to the limited length of URLs, we only pass the web3AuthTokenKey
if (code === null) {
code = params.get("web3AuthTokenKey");
code = localStorage.getItem(code);
}
// Steam don't use code, so we should use all params as code.
if (isSteam !== null && code === null) {
code = this.props.location.search;

View File

@@ -68,7 +68,7 @@ class ForgetPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.onUpdateApplication(res);
this.onUpdateApplication(res.data);
});
}
getApplicationObj() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -317,6 +317,10 @@ const authInfo = {
scope: "user:read",
endpoint: "https://zoom.us/oauth/authorize",
},
MetaMask: {
scope: "",
endpoint: "",
},
};
export function getProviderUrl(provider) {
@@ -459,5 +463,7 @@ export function getAuthUrl(application, provider, method) {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}&grant_options[]=per-user`;
} else if (provider.type === "Twitter" || provider.type === "Fitbit") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
} else if (provider.type === "MetaMask") {
return `${redirectUri}?state=${state}`;
}
}

View File

@@ -17,6 +17,7 @@ import i18next from "i18next";
import * as Provider from "./Provider";
import {getProviderLogoURL} from "../Setting";
import {GithubLoginButton, GoogleLoginButton} from "react-social-login-buttons";
import {authViaMetaMask} from "./Web3Auth";
import QqLoginButton from "./QqLoginButton";
import FacebookLoginButton from "./FacebookLoginButton";
import WeiboLoginButton from "./WeiboLoginButton";
@@ -117,6 +118,12 @@ function goToSamlUrl(provider, location) {
});
}
export function goToWeb3Url(application, provider, method) {
if (provider.type === "MetaMask") {
authViaMetaMask(application, provider, method);
}
}
export function renderProviderLogo(provider, application, width, margin, size, location) {
if (size === "small") {
if (provider.category === "OAuth") {
@@ -153,6 +160,12 @@ export function renderProviderLogo(provider, application, width, margin, size, l
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
</a>
);
} else if (provider.category === "Web3") {
return (
<a key={provider.displayName} onClick={() => goToWeb3Url(application, provider, "signup")}>
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
</a>
);
}
} else if (provider.type === "Custom") {
// style definition
@@ -192,6 +205,16 @@ export function renderProviderLogo(provider, application, width, margin, size, l
</a>
</div>
);
} else if (provider.category === "Web3") {
return (
<div key={provider.displayName} style={{marginBottom: "10px"}}>
<a onClick={() => goToWeb3Url(application, provider, "signup")}>
{
getSigninButton(provider)
}
</a>
</div>
);
} else {
return (
<div key={provider.displayName} style={{marginBottom: "10px"}}>

View File

@@ -48,9 +48,10 @@ class ResultPage extends React.Component {
Setting.showMessage("error", res.msg);
return;
}
this.onUpdateApplication(res);
this.onUpdateApplication(res.data);
this.setState({
application: res,
application: res.data,
});
});
}

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