Compare commits

...

10 Commits

Author SHA1 Message Date
June
4c1915b014 fix: make query with like more precise (#1791) 2023-04-26 18:21:13 +08:00
Yaodong Yu
6c2b172aae feat: fix function CheckAccountItemModifyRule (#1789)
* feat: fix function CheckAccountItemModifyRule

* fix: admin changes its own username

* fix: current user changes its own username

* Update user.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-04-26 16:21:58 +08:00
Yaodong Yu
95f4f4cb6d feat: refactor out form package and optimize verification code module (#1787)
* refactor: add forms package and optimize verification code module

* chore: add license

* chore: fix lint

* chore: fix lint

* chore: fix lint

* chore: swagger
2023-04-25 23:05:53 +08:00
Yang Luo
511aefb706 Disable faulty Beego filter 2023-04-25 20:02:13 +08:00
OutOfEastGate
1003639e5b feat: support for prometheus (#1784) 2023-04-25 16:06:09 +08:00
Yaodong Yu
fe53e90d37 fix: signup page of the app-built-in failed to load (#1785) 2023-04-25 16:00:24 +08:00
OutOfEastGate
8c73cb5395 fix: fix golangci-lint (#1775) 2023-04-23 17:02:29 +08:00
Yang Luo
06ebc04032 Can add/delete chat 2023-04-23 01:19:44 +08:00
Yang Luo
0ee98e2582 Add loading to chat box 2023-04-23 00:25:09 +08:00
Yang Luo
d25508fa56 Improve chat UI 2023-04-22 23:20:40 +08:00
57 changed files with 1139 additions and 704 deletions

View File

@@ -66,7 +66,7 @@ jobs:
- uses: actions/setup-go@v4
with:
go-version: '^1.16.5'
cache-dependency-path: ./go.mod
cache: false
# gen a dummy config file
- run: touch dummy.yml

View File

@@ -121,6 +121,8 @@ p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *
p, *, *, GET, /api/get-prometheus-info, *, *
p, *, *, *, /api/metrics, *, *
`
sa := stringadapter.NewAdapter(ruleText)

View File

@@ -21,6 +21,7 @@ import (
"strconv"
"strings"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@@ -34,44 +35,6 @@ const (
ResponseTypeCas = "cas"
)
type RequestForm struct {
Type string `json:"type"`
Organization string `json:"organization"`
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
Phone string `json:"phone"`
Affiliation string `json:"affiliation"`
IdCard string `json:"idCard"`
Region string `json:"region"`
Application string `json:"application"`
ClientId string `json:"clientId"`
Provider string `json:"provider"`
Code string `json:"code"`
State string `json:"state"`
RedirectUri string `json:"redirectUri"`
Method string `json:"method"`
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`
CountryCode string `json:"countryCode"`
AutoSignin bool `json:"autoSignin"`
RelayState string `json:"relayState"`
SamlRequest string `json:"samlRequest"`
SamlResponse string `json:"samlResponse"`
CaptchaType string `json:"captchaType"`
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
}
type Response struct {
Status string `json:"status"`
Msg string `json:"msg"`
@@ -108,28 +71,28 @@ func (c *ApiController) Signup() {
return
}
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
var authForm form.AuthForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
if err != nil {
c.ResponseError(err.Error())
return
}
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if !application.EnableSignUp {
c.ResponseError(c.T("account:The application does not allow to sign up new account"))
return
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", form.Organization))
msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.FirstName, form.LastName, form.Email, form.Phone, form.CountryCode, form.Affiliation, c.GetAcceptLanguage())
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", authForm.Organization))
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
return
}
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && form.Email != "" {
checkResult := object.CheckVerificationCode(form.Email, form.EmailCode, c.GetAcceptLanguage())
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
checkResult := object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
if checkResult.Code != object.VerificationSuccess {
c.ResponseError(checkResult.Msg)
return
@@ -137,9 +100,9 @@ func (c *ApiController) Signup() {
}
var checkPhone string
if application.IsSignupItemVisible("Phone") && application.GetSignupItemRule("Phone") != "No verification" && form.Phone != "" {
checkPhone, _ = util.GetE164Number(form.Phone, form.CountryCode)
checkResult := object.CheckVerificationCode(checkPhone, form.PhoneCode, c.GetAcceptLanguage())
if application.IsSignupItemVisible("Phone") && application.GetSignupItemRule("Phone") != "No verification" && authForm.Phone != "" {
checkPhone, _ = util.GetE164Number(authForm.Phone, authForm.CountryCode)
checkResult := object.CheckVerificationCode(checkPhone, authForm.PhoneCode, c.GetAcceptLanguage())
if checkResult.Code != object.VerificationSuccess {
c.ResponseError(checkResult.Msg)
return
@@ -148,7 +111,7 @@ func (c *ApiController) Signup() {
id := util.GenerateId()
if application.GetSignupItemRule("ID") == "Incremental" {
lastUser := object.GetLastUser(form.Organization)
lastUser := object.GetLastUser(authForm.Organization)
lastIdInt := -1
if lastUser != nil {
@@ -158,7 +121,7 @@ func (c *ApiController) Signup() {
id = strconv.Itoa(lastIdInt + 1)
}
username := form.Username
username := authForm.Username
if !application.IsSignupItemVisible("Username") {
username = id
}
@@ -170,21 +133,21 @@ func (c *ApiController) Signup() {
}
user := &object.User{
Owner: form.Organization,
Owner: authForm.Organization,
Name: username,
CreatedTime: util.GetCurrentTime(),
Id: id,
Type: "normal-user",
Password: form.Password,
DisplayName: form.Name,
Password: authForm.Password,
DisplayName: authForm.Name,
Avatar: organization.DefaultAvatar,
Email: form.Email,
Phone: form.Phone,
CountryCode: form.CountryCode,
Email: authForm.Email,
Phone: authForm.Phone,
CountryCode: authForm.CountryCode,
Address: []string{},
Affiliation: form.Affiliation,
IdCard: form.IdCard,
Region: form.Region,
Affiliation: authForm.Affiliation,
IdCard: authForm.IdCard,
Region: authForm.Region,
Score: initScore,
IsAdmin: false,
IsGlobalAdmin: false,
@@ -203,10 +166,10 @@ func (c *ApiController) Signup() {
}
if application.GetSignupItemRule("Display name") == "First, last" {
if form.FirstName != "" || form.LastName != "" {
user.DisplayName = fmt.Sprintf("%s %s", form.FirstName, form.LastName)
user.FirstName = form.FirstName
user.LastName = form.LastName
if authForm.FirstName != "" || authForm.LastName != "" {
user.DisplayName = fmt.Sprintf("%s %s", authForm.FirstName, authForm.LastName)
user.FirstName = authForm.FirstName
user.LastName = authForm.LastName
}
}
@@ -223,7 +186,7 @@ func (c *ApiController) Signup() {
c.SetSessionUsername(user.GetId())
}
object.DisableVerificationCode(form.Email)
object.DisableVerificationCode(authForm.Email)
object.DisableVerificationCode(checkPhone)
record := object.NewRecord(c.Ctx)

View File

@@ -28,6 +28,7 @@ import (
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
@@ -56,7 +57,7 @@ func tokenToResponse(token *object.Token) *Response {
}
// HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
userId := user.GetId()
allowed, err := object.CheckAccessPermission(userId, application)
@@ -221,21 +222,21 @@ func isProxyProviderType(providerType string) bool {
// @Param nonce query string false nonce
// @Param code_challenge_method query string false code_challenge_method
// @Param code_challenge query string false code_challenge
// @Param form body controllers.RequestForm true "Login information"
// @Param form body controllers.AuthForm true "Login information"
// @Success 200 {object} Response The Response object
// @router /login [post]
func (c *ApiController) Login() {
resp := &Response{}
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
var authForm form.AuthForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
if err != nil {
c.ResponseError(err.Error())
return
}
if form.Username != "" {
if form.Type == ResponseTypeLogin {
if authForm.Username != "" {
if authForm.Type == ResponseTypeLogin {
if c.GetSessionUsername() != "" {
c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername())
return
@@ -245,25 +246,25 @@ func (c *ApiController) Login() {
var user *object.User
var msg string
if form.Password == "" {
if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
if authForm.Password == "" {
if user = object.GetUserByFields(authForm.Organization, authForm.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(authForm.Organization, authForm.Username)))
return
}
verificationCodeType := object.GetVerifyType(form.Username)
verificationCodeType := object.GetVerifyType(authForm.Username)
var checkDest string
if verificationCodeType == object.VerifyTypePhone {
form.CountryCode = user.GetCountryCode(form.CountryCode)
authForm.CountryCode = user.GetCountryCode(authForm.CountryCode)
var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), form.CountryCode))
if checkDest, ok = util.GetE164Number(authForm.Username, authForm.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), authForm.CountryCode))
return
}
}
// check result through Email or Phone
checkResult := object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage())
checkResult := object.CheckSigninCode(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
return
@@ -272,9 +273,9 @@ func (c *ApiController) Login() {
// disable the verification code
object.DisableVerificationCode(checkDest)
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
if !application.EnablePassword {
@@ -282,8 +283,8 @@ func (c *ApiController) Login() {
return
}
var enableCaptcha bool
if enableCaptcha = object.CheckToEnableCaptcha(application, form.Organization, form.Username); enableCaptcha {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(form.CaptchaType, form.CaptchaToken, form.ClientSecret)
if enableCaptcha = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); enableCaptcha {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
@@ -295,42 +296,42 @@ func (c *ApiController) Login() {
}
}
password := form.Password
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage(), enableCaptcha)
password := authForm.Password
user, msg = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
}
if msg != "" {
resp = &Response{Status: "error", Msg: msg}
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
}
} else if form.Provider != "" {
} else if authForm.Provider != "" {
var application *object.Application
if form.ClientId != "" {
application = object.GetApplicationByClientId(form.ClientId)
if authForm.ClientId != "" {
application = object.GetApplicationByClientId(authForm.ClientId)
} else {
application = object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", application.Organization))
provider := object.GetProvider(util.GetId("admin", form.Provider))
provider := object.GetProvider(util.GetId("admin", authForm.Provider))
providerItem := application.GetProviderItem(provider.Name)
if !providerItem.IsProviderVisible() {
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s is not enabled for the application"), provider.Name))
@@ -340,7 +341,7 @@ func (c *ApiController) Login() {
userInfo := &idp.UserInfo{}
if provider.Category == "SAML" {
// SAML
userInfo.Id, err = object.ParseSamlResponse(form.SamlResponse, provider, c.Ctx.Request.Host)
userInfo.Id, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error())
return
@@ -355,7 +356,7 @@ func (c *ApiController) Login() {
clientSecret = provider.ClientSecret2
}
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl)
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, authForm.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl)
if idProvider == nil {
c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type))
return
@@ -363,13 +364,13 @@ func (c *ApiController) Login() {
setHttpClient(idProvider, provider.Type)
if form.State != conf.GetConfigString("authState") && form.State != application.Name {
c.ResponseError(fmt.Sprintf(c.T("auth:State expected: %s, but got: %s"), conf.GetConfigString("authState"), form.State))
if authForm.State != conf.GetConfigString("authState") && authForm.State != application.Name {
c.ResponseError(fmt.Sprintf(c.T("auth:State expected: %s, but got: %s"), conf.GetConfigString("authState"), authForm.State))
return
}
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
token, err := idProvider.GetToken(form.Code)
token, err := idProvider.GetToken(authForm.Code)
if err != nil {
c.ResponseError(err.Error())
return
@@ -387,7 +388,7 @@ func (c *ApiController) Login() {
}
}
if form.Method == "signup" {
if authForm.Method == "signup" {
user := &object.User{}
if provider.Category == "SAML" {
user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id))
@@ -402,7 +403,7 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
}
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
@@ -477,7 +478,7 @@ func (c *ApiController) Login() {
object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
object.LinkUserAccount(user, provider.Type, userInfo.Id)
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
@@ -493,7 +494,7 @@ func (c *ApiController) Login() {
resp = &Response{Status: "error", Msg: fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id))}
}
// resp = &Response{Status: "ok", Msg: "", Data: res}
} else { // form.Method != "signup"
} else { // authForm.Method != "signup"
userId := c.GetSessionUsername()
if userId == "" {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id)), userInfo)
@@ -521,21 +522,21 @@ func (c *ApiController) Login() {
} else {
if c.GetSessionUsername() != "" {
// user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else {
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), form = %s"), util.StructToJson(form)))
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), authForm = %s"), util.StructToJson(authForm)))
return
}
}

View File

@@ -41,18 +41,44 @@ type SessionData struct {
}
func (c *ApiController) IsGlobalAdmin() bool {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {
// e.g., "app/app-casnode"
return true
}
isGlobalAdmin, _ := c.isGlobalAdmin()
user := object.GetUser(username)
return isGlobalAdmin
}
func (c *ApiController) IsAdmin() bool {
isGlobalAdmin, user := c.isGlobalAdmin()
if user == nil {
return false
}
return user.Owner == "built-in" || user.IsGlobalAdmin
return isGlobalAdmin || user.IsAdmin
}
func (c *ApiController) isGlobalAdmin() (bool, *object.User) {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {
// e.g., "app/app-casnode"
return true, nil
}
user := c.getCurrentUser()
if user == nil {
return false, nil
}
return user.Owner == "built-in" || user.IsGlobalAdmin, user
}
func (c *ApiController) getCurrentUser() *object.User {
var user *object.User
userId := c.GetSessionUsername()
if userId == "" {
user = nil
} else {
user = object.GetUser(userId)
}
return user
}
// GetSessionUsername ...

39
controllers/prometheus.go Normal file
View File

@@ -0,0 +1,39 @@
// 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 controllers
import (
"github.com/casdoor/casdoor/object"
)
// GetPrometheusInfo
// @Title GetPrometheusInfo
// @Tag Prometheus API
// @Description get Prometheus Info
// @Success 200 {object} object.PrometheusInfo The Response object
// @router /get-prometheus-info [get]
func (c *ApiController) GetPrometheusInfo() {
_, ok := c.RequireAdmin()
if !ok {
return
}
prometheusInfo, err := object.GetPrometheusInfo()
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(prometheusInfo)
}

View File

@@ -162,7 +162,9 @@ func (c *ApiController) UpdateUser() {
c.ResponseError(msg)
return
}
if pass, err := checkPermissionForUpdateUser(oldUser, &user, c); !pass {
isAdmin := c.IsAdmin()
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, c.GetAcceptLanguage()); !pass {
c.ResponseError(err)
return
}
@@ -172,9 +174,7 @@ func (c *ApiController) UpdateUser() {
columns = strings.Split(columnsStr, ",")
}
isGlobalAdmin := c.IsGlobalAdmin()
affected := object.UpdateUser(id, &user, columns, isGlobalAdmin)
affected := object.UpdateUser(id, &user, columns, isAdmin)
if affected {
object.UpdateUserToOriginalDatabase(&user)
}

View File

@@ -1,138 +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.
package controllers
import (
"encoding/json"
"github.com/casdoor/casdoor/object"
)
func checkPermissionForUpdateUser(oldUser, newUser *object.User, c *ApiController) (bool, string) {
organization := object.GetOrganizationByUser(oldUser)
var itemsChanged []*object.AccountItem
if oldUser.Owner != newUser.Owner {
item := object.GetAccountItemByName("Organization", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Name != newUser.Name {
item := object.GetAccountItemByName("Name", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Id != newUser.Id {
item := object.GetAccountItemByName("ID", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.DisplayName != newUser.DisplayName {
item := object.GetAccountItemByName("Display name", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Avatar != newUser.Avatar {
item := object.GetAccountItemByName("Avatar", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Type != newUser.Type {
item := object.GetAccountItemByName("User type", organization)
itemsChanged = append(itemsChanged, item)
}
// The password is *** when not modified
if oldUser.Password != newUser.Password && newUser.Password != "***" {
item := object.GetAccountItemByName("Password", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Email != newUser.Email {
item := object.GetAccountItemByName("Email", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Phone != newUser.Phone {
item := object.GetAccountItemByName("Phone", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.CountryCode != newUser.CountryCode {
item := object.GetAccountItemByName("Country code", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Region != newUser.Region {
item := object.GetAccountItemByName("Country/Region", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Location != newUser.Location {
item := object.GetAccountItemByName("Location", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Affiliation != newUser.Affiliation {
item := object.GetAccountItemByName("Affiliation", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Title != newUser.Title {
item := object.GetAccountItemByName("Title", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Homepage != newUser.Homepage {
item := object.GetAccountItemByName("Homepage", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Bio != newUser.Bio {
item := object.GetAccountItemByName("Bio", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Tag != newUser.Tag {
item := object.GetAccountItemByName("Tag", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.SignupApplication != newUser.SignupApplication {
item := object.GetAccountItemByName("Signup application", organization)
itemsChanged = append(itemsChanged, item)
}
oldUserPropertiesJson, _ := json.Marshal(oldUser.Properties)
newUserPropertiesJson, _ := json.Marshal(newUser.Properties)
if string(oldUserPropertiesJson) != string(newUserPropertiesJson) {
item := object.GetAccountItemByName("Properties", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsAdmin != newUser.IsAdmin {
item := object.GetAccountItemByName("Is admin", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsGlobalAdmin != newUser.IsGlobalAdmin {
item := object.GetAccountItemByName("Is global admin", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsForbidden != newUser.IsForbidden {
item := object.GetAccountItemByName("Is forbidden", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsDeleted != newUser.IsDeleted {
item := object.GetAccountItemByName("Is deleted", organization)
itemsChanged = append(itemsChanged, item)
}
currentUser := c.getCurrentUser()
if currentUser == nil && c.IsGlobalAdmin() {
currentUser = &object.User{
IsGlobalAdmin: true,
}
}
for i := range itemsChanged {
if pass, err := object.CheckAccountItemModifyRule(itemsChanged[i], currentUser, c.GetAcceptLanguage()); !pass {
return pass, err
}
}
return true, ""
}

View File

@@ -21,6 +21,7 @@ import (
"strings"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@@ -32,60 +33,29 @@ const (
ForgetVerification = "forget"
)
func (c *ApiController) getCurrentUser() *object.User {
var user *object.User
userId := c.GetSessionUsername()
if userId == "" {
user = nil
} else {
user = object.GetUser(userId)
}
return user
}
// SendVerificationCode ...
// @Title SendVerificationCode
// @Tag Verification API
// @router /send-verification-code [post]
func (c *ApiController) SendVerificationCode() {
destType := c.Ctx.Request.Form.Get("type")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("countryCode")
checkType := c.Ctx.Request.Form.Get("checkType")
clientSecret := c.Ctx.Request.Form.Get("clientSecret")
captchaToken := c.Ctx.Request.Form.Get("captchaToken")
applicationId := c.Ctx.Request.Form.Get("applicationId")
method := c.Ctx.Request.Form.Get("method")
checkUser := c.Ctx.Request.Form.Get("checkUser")
var vform form.VerificationForm
err := c.ParseForm(&vform)
if err != nil {
c.ResponseError(err.Error())
return
}
remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if dest == "" {
c.ResponseError(c.T("general:Missing parameter") + ": dest.")
return
}
if applicationId == "" {
c.ResponseError(c.T("general:Missing parameter") + ": applicationId.")
return
}
if checkType == "" {
c.ResponseError(c.T("general:Missing parameter") + ": checkType.")
return
}
if !strings.Contains(applicationId, "/") {
c.ResponseError(c.T("verification:Wrong parameter") + ": applicationId.")
if msg := vform.CheckParameter(form.SendVerifyCode, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg)
return
}
if checkType != "none" {
if captchaToken == "" {
c.ResponseError(c.T("general:Missing parameter") + ": captchaToken.")
if vform.CaptchaType != "none" {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
}
if captchaProvider := captcha.GetCaptchaProvider(checkType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + checkType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(captchaToken, clientSecret); err != nil {
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
@@ -94,7 +64,7 @@ func (c *ApiController) SendVerificationCode() {
}
}
application := object.GetApplication(applicationId)
application := object.GetApplication(vform.ApplicationId)
organization := object.GetOrganization(util.GetId(application.Owner, application.Organization))
if organization == nil {
c.ResponseError(c.T("check:Organization does not exist"))
@@ -103,57 +73,57 @@ func (c *ApiController) SendVerificationCode() {
var user *object.User
// checkUser != "", means method is ForgetVerification
if checkUser != "" {
if vform.CheckUser != "" {
owner := application.Organization
user = object.GetUser(util.GetId(owner, checkUser))
user = object.GetUser(util.GetId(owner, vform.CheckUser))
}
sendResp := errors.New("invalid dest type")
switch destType {
switch vform.Type {
case object.VerifyTypeEmail:
if !util.IsEmailValid(dest) {
if !util.IsEmailValid(vform.Dest) {
c.ResponseError(c.T("check:Email is invalid"))
return
}
if method == LoginVerification || method == ForgetVerification {
if user != nil && util.GetMaskedEmail(user.Email) == dest {
dest = user.Email
if vform.Method == LoginVerification || vform.Method == ForgetVerification {
if user != nil && util.GetMaskedEmail(user.Email) == vform.Dest {
vform.Dest = user.Email
}
user = object.GetUserByEmail(organization.Name, dest)
user = object.GetUserByEmail(organization.Name, vform.Dest)
if user == nil {
c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return
}
} else if method == ResetVerification {
} else if vform.Method == ResetVerification {
user = c.getCurrentUser()
}
provider := application.GetEmailProvider()
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, vform.Dest)
case object.VerifyTypePhone:
if method == LoginVerification || method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == dest {
dest = user.Phone
if vform.Method == LoginVerification || vform.Method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {
vform.Dest = user.Phone
}
if user = object.GetUserByPhone(organization.Name, dest); user == nil {
if user = object.GetUserByPhone(organization.Name, vform.Dest); user == nil {
c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return
}
countryCode = user.GetCountryCode(countryCode)
} else if method == ResetVerification {
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} else if vform.Method == ResetVerification {
if user = c.getCurrentUser(); user != nil {
countryCode = user.GetCountryCode(countryCode)
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
}
}
provider := application.GetSmsProvider()
if phone, ok := util.GetE164Number(dest, countryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), countryCode))
if phone, ok := util.GetE164Number(vform.Dest, vform.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))
return
} else {
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, phone)
@@ -167,6 +137,38 @@ func (c *ApiController) SendVerificationCode() {
}
}
// VerifyCaptcha ...
// @Title VerifyCaptcha
// @Tag Verification API
// @router /verify-captcha [post]
func (c *ApiController) VerifyCaptcha() {
var vform form.VerificationForm
err := c.ParseForm(&vform)
if err != nil {
c.ResponseError(err.Error())
return
}
if msg := vform.CheckParameter(form.VerifyCaptcha, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg)
return
}
provider := captcha.GetCaptchaProvider(vform.CaptchaType)
if provider == nil {
c.ResponseError(c.T("verification:Invalid captcha provider."))
return
}
isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(isValid)
}
// ResetEmailOrPhone ...
// @Tag Account API
// @Title ResetEmailOrPhone
@@ -200,7 +202,7 @@ func (c *ApiController) ResetEmailOrPhone() {
return
}
if pass, errMsg := object.CheckAccountItemModifyRule(phoneItem, user, c.GetAcceptLanguage()); !pass {
if pass, errMsg := object.CheckAccountItemModifyRule(phoneItem, user.IsAdminUser(), c.GetAcceptLanguage()); !pass {
c.ResponseError(errMsg)
return
}
@@ -220,11 +222,12 @@ func (c *ApiController) ResetEmailOrPhone() {
return
}
if pass, errMsg := object.CheckAccountItemModifyRule(emailItem, user, c.GetAcceptLanguage()); !pass {
if pass, errMsg := object.CheckAccountItemModifyRule(emailItem, user.IsAdminUser(), c.GetAcceptLanguage()); !pass {
c.ResponseError(errMsg)
return
}
}
if result := object.CheckVerificationCode(checkDest, code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
@@ -247,88 +250,55 @@ func (c *ApiController) ResetEmailOrPhone() {
}
// VerifyCode
// @Tag Account API
// @Tag Verification API
// @Title VerifyCode
// @router /api/verify-code [post]
func (c *ApiController) VerifyCode() {
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
var authForm form.AuthForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
if err != nil {
c.ResponseError(err.Error())
return
}
var user *object.User
if form.Name != "" {
user = object.GetUserByFields(form.Organization, form.Name)
if authForm.Name != "" {
user = object.GetUserByFields(authForm.Organization, authForm.Name)
}
var checkDest string
if strings.Contains(form.Username, "@") {
if user != nil && util.GetMaskedEmail(user.Email) == form.Username {
form.Username = user.Email
if strings.Contains(authForm.Username, "@") {
if user != nil && util.GetMaskedEmail(user.Email) == authForm.Username {
authForm.Username = user.Email
}
checkDest = form.Username
checkDest = authForm.Username
} else {
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.Username = user.Phone
if user != nil && util.GetMaskedPhone(user.Phone) == authForm.Username {
authForm.Username = user.Phone
}
}
if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
if user = object.GetUserByFields(authForm.Organization, authForm.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(authForm.Organization, authForm.Username)))
return
}
verificationCodeType := object.GetVerifyType(form.Username)
verificationCodeType := object.GetVerifyType(authForm.Username)
if verificationCodeType == object.VerifyTypePhone {
form.CountryCode = user.GetCountryCode(form.CountryCode)
authForm.CountryCode = user.GetCountryCode(authForm.CountryCode)
var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), form.CountryCode))
if checkDest, ok = util.GetE164Number(authForm.Username, authForm.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), authForm.CountryCode))
return
}
}
if result := object.CheckVerificationCode(checkDest, form.Code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess {
if result := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
}
object.DisableVerificationCode(checkDest)
c.SetSession("verifiedCode", form.Code)
c.SetSession("verifiedCode", authForm.Code)
c.ResponseOk()
}
// VerifyCaptcha ...
// @Title VerifyCaptcha
// @Tag Verification API
// @router /verify-captcha [post]
func (c *ApiController) VerifyCaptcha() {
captchaType := c.Ctx.Request.Form.Get("captchaType")
captchaToken := c.Ctx.Request.Form.Get("captchaToken")
clientSecret := c.Ctx.Request.Form.Get("clientSecret")
if captchaToken == "" {
c.ResponseError(c.T("general:Missing parameter") + ": captchaToken.")
return
}
if clientSecret == "" {
c.ResponseError(c.T("general:Missing parameter") + ": clientSecret.")
return
}
provider := captcha.GetCaptchaProvider(captchaType)
if provider == nil {
c.ResponseError(c.T("verification:Invalid captcha provider."))
return
}
isValid, err := provider.VerifyCaptcha(captchaToken, clientSecret)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(isValid)
}

View File

@@ -19,6 +19,7 @@ import (
"fmt"
"io"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/go-webauthn/webauthn/protocol"
@@ -147,9 +148,9 @@ func (c *ApiController) WebAuthnSigninFinish() {
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
application := object.GetApplicationByUser(user)
var form RequestForm
form.Type = responseType
resp := c.HandleLoggedIn(application, user, &form)
var authForm form.AuthForm
authForm.Type = responseType
resp := c.HandleLoggedIn(application, user, &authForm)
c.Data["json"] = resp
c.ServeJSON()
}

53
form/auth.go Normal file
View File

@@ -0,0 +1,53 @@
// 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 form
type AuthForm struct {
Type string `json:"type"`
Organization string `json:"organization"`
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
Phone string `json:"phone"`
Affiliation string `json:"affiliation"`
IdCard string `json:"idCard"`
Region string `json:"region"`
Application string `json:"application"`
ClientId string `json:"clientId"`
Provider string `json:"provider"`
Code string `json:"code"`
State string `json:"state"`
RedirectUri string `json:"redirectUri"`
Method string `json:"method"`
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`
CountryCode string `json:"countryCode"`
AutoSignin bool `json:"autoSignin"`
RelayState string `json:"relayState"`
SamlRequest string `json:"samlRequest"`
SamlResponse string `json:"samlResponse"`
CaptchaType string `json:"captchaType"`
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
}

67
form/verification.go Normal file
View File

@@ -0,0 +1,67 @@
// 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 form
import (
"strings"
"github.com/casdoor/casdoor/i18n"
)
type VerificationForm struct {
Dest string `form:"dest"`
Type string `form:"type"`
CountryCode string `form:"countryCode"`
ApplicationId string `form:"applicationId"`
Method string `form:"method"`
CheckUser string `form:"checkUser"`
CaptchaType string `form:"captchaType"`
ClientSecret string `form:"clientSecret"`
CaptchaToken string `form:"captchaToken"`
}
const (
SendVerifyCode = 0
VerifyCaptcha = 1
)
func (form *VerificationForm) CheckParameter(checkType int, lang string) string {
if checkType == SendVerifyCode {
if form.Type == "" {
return i18n.Translate(lang, "general:Missing parameter") + ": type."
}
if form.Dest == "" {
return i18n.Translate(lang, "general:Missing parameter") + ": dest."
}
if form.CaptchaType == "" {
return i18n.Translate(lang, "general:Missing parameter") + ": checkType."
}
if !strings.Contains(form.ApplicationId, "/") {
return i18n.Translate(lang, "verification:Wrong parameter") + ": applicationId."
}
}
if form.CaptchaType != "none" {
if form.CaptchaToken == "" {
return i18n.Translate(lang, "general:Missing parameter") + ": captchaToken."
}
if form.ClientSecret == "" {
return i18n.Translate(lang, "general:Missing parameter") + ": clientSecret."
}
}
return ""
}

2
go.mod
View File

@@ -36,6 +36,8 @@ require (
github.com/markbates/goth v1.75.2
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5
github.com/prometheus/client_golang v1.7.0
github.com/prometheus/client_model v0.2.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0

View File

@@ -82,6 +82,7 @@ func main() {
logs.SetLogFuncCall(false)
go ldap.StartLdapServer()
go object.ClearThroughputPerSecond()
beego.Run(fmt.Sprintf(":%v", port))
}

View File

@@ -135,6 +135,10 @@ func DeleteChat(chat *Chat) bool {
panic(err)
}
if affected != 0 {
return DeleteChatMessages(chat.Name)
}
return affected != 0
}

View File

@@ -22,6 +22,7 @@ import (
"unicode"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
@@ -42,86 +43,86 @@ func init() {
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, firstName string, lastName string, email string, phone string, countryCode string, affiliation string, lang string) string {
func CheckUserSignup(application *Application, organization *Organization, form *form.AuthForm, lang string) string {
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
}
if application.IsSignupItemVisible("Username") {
if len(username) <= 1 {
if len(form.Username) <= 1 {
return i18n.Translate(lang, "check:Username must have at least 2 characters")
}
if unicode.IsDigit(rune(username[0])) {
if unicode.IsDigit(rune(form.Username[0])) {
return i18n.Translate(lang, "check:Username cannot start with a digit")
}
if util.IsEmailValid(username) {
if util.IsEmailValid(form.Username) {
return i18n.Translate(lang, "check:Username cannot be an email address")
}
if reWhiteSpace.MatchString(username) {
if reWhiteSpace.MatchString(form.Username) {
return i18n.Translate(lang, "check:Username cannot contain white spaces")
}
if msg := CheckUsername(username, lang); msg != "" {
if msg := CheckUsername(form.Username, lang); msg != "" {
return msg
}
if HasUserByField(organization.Name, "name", username) {
if HasUserByField(organization.Name, "name", form.Username) {
return i18n.Translate(lang, "check:Username already exists")
}
if HasUserByField(organization.Name, "email", email) {
if HasUserByField(organization.Name, "email", form.Email) {
return i18n.Translate(lang, "check:Email already exists")
}
if HasUserByField(organization.Name, "phone", phone) {
if HasUserByField(organization.Name, "phone", form.Phone) {
return i18n.Translate(lang, "check:Phone already exists")
}
}
if len(password) <= 5 {
if len(form.Password) <= 5 {
return i18n.Translate(lang, "check:Password must have at least 6 characters")
}
if application.IsSignupItemVisible("Email") {
if email == "" {
if form.Email == "" {
if application.IsSignupItemRequired("Email") {
return i18n.Translate(lang, "check:Email cannot be empty")
}
} else {
if HasUserByField(organization.Name, "email", email) {
if HasUserByField(organization.Name, "email", form.Email) {
return i18n.Translate(lang, "check:Email already exists")
} else if !util.IsEmailValid(email) {
} else if !util.IsEmailValid(form.Email) {
return i18n.Translate(lang, "check:Email is invalid")
}
}
}
if application.IsSignupItemVisible("Phone") {
if phone == "" {
if form.Phone == "" {
if application.IsSignupItemRequired("Phone") {
return i18n.Translate(lang, "check:Phone cannot be empty")
}
} else {
if HasUserByField(organization.Name, "phone", phone) {
if HasUserByField(organization.Name, "phone", form.Phone) {
return i18n.Translate(lang, "check:Phone already exists")
} else if !util.IsPhoneAllowInRegin(countryCode, organization.CountryCodes) {
} else if !util.IsPhoneAllowInRegin(form.CountryCode, organization.CountryCodes) {
return i18n.Translate(lang, "check:Your region is not allow to signup by phone")
} else if !util.IsPhoneValid(phone, countryCode) {
} else if !util.IsPhoneValid(form.Phone, form.CountryCode) {
return i18n.Translate(lang, "check:Phone number is invalid")
}
}
}
if application.IsSignupItemVisible("Display name") {
if application.GetSignupItemRule("Display name") == "First, last" && (firstName != "" || lastName != "") {
if firstName == "" {
if application.GetSignupItemRule("Display name") == "First, last" && (form.FirstName != "" || form.LastName != "") {
if form.FirstName == "" {
return i18n.Translate(lang, "check:FirstName cannot be blank")
} else if lastName == "" {
} else if form.LastName == "" {
return i18n.Translate(lang, "check:LastName cannot be blank")
}
} else {
if displayName == "" {
if form.Name == "" {
return i18n.Translate(lang, "check:DisplayName cannot be blank")
} else if application.GetSignupItemRule("Display name") == "Real name" {
if !isValidRealName(displayName) {
if !isValidRealName(form.Name) {
return i18n.Translate(lang, "check:DisplayName is not valid real name")
}
}
@@ -129,7 +130,7 @@ func CheckUserSignup(application *Application, organization *Organization, usern
}
if application.IsSignupItemVisible("Affiliation") {
if affiliation == "" {
if form.Affiliation == "" {
return i18n.Translate(lang, "check:Affiliation cannot be blank")
}
}

View File

@@ -143,6 +143,15 @@ func DeleteMessage(message *Message) bool {
return affected != 0
}
func DeleteChatMessages(chat string) bool {
affected, err := adapter.Engine.Delete(&Message{Chat: chat})
if err != nil {
panic(err)
}
return affected != 0
}
func (p *Message) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

View File

@@ -209,14 +209,14 @@ func GetAccountItemByName(name string, organization *Organization) *AccountItem
return nil
}
func CheckAccountItemModifyRule(accountItem *AccountItem, user *User, lang string) (bool, string) {
func CheckAccountItemModifyRule(accountItem *AccountItem, isAdmin bool, lang string) (bool, string) {
if accountItem == nil {
return true, ""
}
switch accountItem.ModifyRule {
case "Admin":
if user == nil || !user.IsAdmin && !user.IsGlobalAdmin {
if !isAdmin {
return false, fmt.Sprintf(i18n.Translate(lang, "organization:Only admin can modify the %s."), accountItem.Name)
}
case "Immutable":

View File

@@ -239,7 +239,7 @@ func DeletePermission(permission *Permission) bool {
func GetPermissionsByUser(userId string) []*Permission {
permissions := []*Permission{}
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&permissions)
err := adapter.Engine.Where("users like ?", "%"+userId+"\"%").Find(&permissions)
if err != nil {
panic(err)
}
@@ -253,7 +253,7 @@ func GetPermissionsByUser(userId string) []*Permission {
func GetPermissionsByRole(roleId string) []*Permission {
permissions := []*Permission{}
err := adapter.Engine.Where("roles like ?", "%"+roleId+"%").Find(&permissions)
err := adapter.Engine.Where("roles like ?", "%"+roleId+"\"%").Find(&permissions)
if err != nil {
panic(err)
}

129
object/prometheus.go Normal file
View File

@@ -0,0 +1,129 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_model/go"
)
type PrometheusInfo struct {
APIThroughput []GaugeVecInfo `json:"apiThroughput"`
APILatency []HistogramVecInfo `json:"apiLatency"`
TotalThroughput float64 `json:"totalThroughput"`
}
type GaugeVecInfo struct {
Method string `json:"method"`
Name string `json:"name"`
Throughput float64 `json:"throughput"`
}
type HistogramVecInfo struct {
Name string `json:"name"`
Method string `json:"method"`
Count uint64 `json:"count"`
Latency string `json:"latency"`
}
var (
APIThroughput = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "casdoor_api_throughput",
Help: "The throughput of each api access",
}, []string{"path", "method"})
APILatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "casdoor_api_latency",
Help: "API processing latency in milliseconds",
}, []string{"path", "method"})
CpuUsage = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "casdoor_cpu_usage",
Help: "Casdoor cpu usage",
}, []string{"cpuNum"})
MemoryUsage = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "casdoor_memory_usage",
Help: "Casdoor memory usage in Byte",
}, []string{"type"})
TotalThroughput = promauto.NewGauge(prometheus.GaugeOpts{
Name: "casdoor_total_throughput",
Help: "The total throughput of casdoor",
})
)
func ClearThroughputPerSecond() {
// Clear the throughput every second
ticker := time.NewTicker(time.Second)
for range ticker.C {
APIThroughput.Reset()
TotalThroughput.Set(0)
}
}
func GetPrometheusInfo() (*PrometheusInfo, error) {
res := &PrometheusInfo{}
metricFamilies, err := prometheus.DefaultGatherer.Gather()
if err != nil {
return nil, err
}
for _, metricFamily := range metricFamilies {
switch metricFamily.GetName() {
case "casdoor_api_throughput":
res.APIThroughput = getGaugeVecInfo(metricFamily)
case "casdoor_api_latency":
res.APILatency = getHistogramVecInfo(metricFamily)
case "casdoor_total_throughput":
res.TotalThroughput = metricFamily.GetMetric()[0].GetGauge().GetValue()
}
}
return res, nil
}
func getHistogramVecInfo(metricFamily *io_prometheus_client.MetricFamily) []HistogramVecInfo {
var histogramVecInfos []HistogramVecInfo
for _, metric := range metricFamily.GetMetric() {
sampleCount := metric.GetHistogram().GetSampleCount()
sampleSum := metric.GetHistogram().GetSampleSum()
latency := sampleSum / float64(sampleCount)
histogramVecInfo := HistogramVecInfo{
Method: metric.Label[0].GetValue(),
Name: metric.Label[1].GetValue(),
Count: sampleCount,
Latency: fmt.Sprintf("%.3f", latency),
}
histogramVecInfos = append(histogramVecInfos, histogramVecInfo)
}
return histogramVecInfos
}
func getGaugeVecInfo(metricFamily *io_prometheus_client.MetricFamily) []GaugeVecInfo {
var counterVecInfos []GaugeVecInfo
for _, metric := range metricFamily.GetMetric() {
counterVecInfo := GaugeVecInfo{
Method: metric.Label[0].GetValue(),
Name: metric.Label[1].GetValue(),
Throughput: metric.Gauge.GetValue(),
}
counterVecInfos = append(counterVecInfos, counterVecInfo)
}
return counterVecInfos
}

View File

@@ -153,7 +153,7 @@ func (role *Role) GetId() string {
func GetRolesByUser(userId string) []*Role {
roles := []*Role{}
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&roles)
err := adapter.Engine.Where("users like ?", "%"+userId+"\"%").Find(&roles)
if err != nil {
panic(err)
}

View File

@@ -425,7 +425,7 @@ func GetLastUser(owner string) *User {
return nil
}
func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) bool {
func UpdateUser(id string, user *User, columns []string, isAdmin bool) bool {
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
oldUser := getUser(owner, name)
if oldUser == nil {
@@ -456,7 +456,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
"signin_wrong_times", "last_signin_wrong_time",
}
}
if isGlobalAdmin {
if isAdmin {
columns = append(columns, "name", "email", "phone", "country_code")
}

View File

@@ -15,6 +15,7 @@
package object
import (
"encoding/json"
"fmt"
"reflect"
"strings"
@@ -179,6 +180,116 @@ func ClearUserOAuthProperties(user *User, providerType string) bool {
return affected != 0
}
func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang string) (bool, string) {
organization := GetOrganizationByUser(oldUser)
var itemsChanged []*AccountItem
if oldUser.Owner != newUser.Owner {
item := GetAccountItemByName("Organization", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Name != newUser.Name {
item := GetAccountItemByName("Name", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Id != newUser.Id {
item := GetAccountItemByName("ID", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.DisplayName != newUser.DisplayName {
item := GetAccountItemByName("Display name", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Avatar != newUser.Avatar {
item := GetAccountItemByName("Avatar", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Type != newUser.Type {
item := GetAccountItemByName("User type", organization)
itemsChanged = append(itemsChanged, item)
}
// The password is *** when not modified
if oldUser.Password != newUser.Password && newUser.Password != "***" {
item := GetAccountItemByName("Password", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Email != newUser.Email {
item := GetAccountItemByName("Email", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Phone != newUser.Phone {
item := GetAccountItemByName("Phone", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.CountryCode != newUser.CountryCode {
item := GetAccountItemByName("Country code", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Region != newUser.Region {
item := GetAccountItemByName("Country/Region", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Location != newUser.Location {
item := GetAccountItemByName("Location", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Affiliation != newUser.Affiliation {
item := GetAccountItemByName("Affiliation", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Title != newUser.Title {
item := GetAccountItemByName("Title", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Homepage != newUser.Homepage {
item := GetAccountItemByName("Homepage", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Bio != newUser.Bio {
item := GetAccountItemByName("Bio", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Tag != newUser.Tag {
item := GetAccountItemByName("Tag", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.SignupApplication != newUser.SignupApplication {
item := GetAccountItemByName("Signup application", organization)
itemsChanged = append(itemsChanged, item)
}
oldUserPropertiesJson, _ := json.Marshal(oldUser.Properties)
newUserPropertiesJson, _ := json.Marshal(newUser.Properties)
if string(oldUserPropertiesJson) != string(newUserPropertiesJson) {
item := GetAccountItemByName("Properties", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsAdmin != newUser.IsAdmin {
item := GetAccountItemByName("Is admin", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsGlobalAdmin != newUser.IsGlobalAdmin {
item := GetAccountItemByName("Is global admin", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsForbidden != newUser.IsForbidden {
item := GetAccountItemByName("Is forbidden", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsDeleted != newUser.IsDeleted {
item := GetAccountItemByName("Is deleted", organization)
itemsChanged = append(itemsChanged, item)
}
for i := range itemsChanged {
if pass, err := CheckAccountItemModifyRule(itemsChanged[i], isAdmin, lang); !pass {
return pass, err
}
}
return true, ""
}
func (user *User) GetCountryCode(countryCode string) string {
if countryCode != "" {
return countryCode
@@ -193,3 +304,11 @@ func (user *User) GetCountryCode(countryCode string) string {
}
return ""
}
func (user *User) IsAdminUser() bool {
if user == nil {
return false
}
return user.IsAdmin || user.IsGlobalAdmin
}

View File

@@ -0,0 +1,51 @@
package routers
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
type PrometheusMiddleWareWrapper struct {
handler http.Handler
}
func PrometheusMiddleWare(h http.Handler) http.Handler {
return &PrometheusMiddleWareWrapper{
handler: h,
}
}
func (p PrometheusMiddleWareWrapper) ServeHTTP(w http.ResponseWriter, req *http.Request) {
method := req.Method
endpoint := req.URL.Path
if strings.HasPrefix(endpoint, "/api/metrics") {
systemInfo, err := util.GetSystemInfo()
if err == nil {
recordSystemInfo(systemInfo)
}
p.handler.ServeHTTP(w, req)
return
}
if strings.HasPrefix(endpoint, "/api") {
start := time.Now()
p.handler.ServeHTTP(w, req)
latency := time.Since(start).Milliseconds()
object.TotalThroughput.Inc()
object.APILatency.WithLabelValues(endpoint, method).Observe(float64(latency))
object.APIThroughput.WithLabelValues(endpoint, method).Inc()
}
}
func recordSystemInfo(systemInfo *util.SystemInfo) {
for i, value := range systemInfo.CpuUsage {
object.CpuUsage.WithLabelValues(fmt.Sprintf("%d", i)).Set(value)
}
object.MemoryUsage.WithLabelValues("memoryUsed").Set(float64(systemInfo.MemoryUsed))
object.MemoryUsage.WithLabelValues("memoryTotal").Set(float64(systemInfo.MemoryTotal))
}

View File

@@ -21,8 +21,8 @@ package routers
import (
"github.com/beego/beego"
"github.com/casdoor/casdoor/controllers"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func init() {
@@ -239,4 +239,7 @@ func initAPI() {
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo")
beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")
beego.Handler("/api/metrics", promhttp.Handler())
}

View File

@@ -551,15 +551,33 @@
"operationId": "ApiController.GetCaptcha"
}
},
"/api/api/get-webhook-event": {
"/api/api/get-captcha-status": {
"get": {
"tags": [
"GetCaptchaStatus API"
"Token API"
],
"operationId": "ApiController.GetCaptchaStatus"
"description": "Get Login Error Counts",
"operationId": "ApiController.GetCaptchaStatus",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of user",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/api/get-captcha-status": {
"/api/api/get-webhook-event": {
"get": {
"tags": [
"GetWebhookEventType API"
@@ -662,7 +680,7 @@
"/api/api/verify-code": {
"post": {
"tags": [
"Account API"
"Verification API"
],
"operationId": "ApiController.VerifyCode"
}
@@ -2752,7 +2770,7 @@
"description": "Login information",
"required": true,
"schema": {
"$ref": "#/definitions/controllers.RequestForm"
"$ref": "#/definitions/controllers.AuthForm"
}
}
],
@@ -3891,11 +3909,11 @@
}
},
"definitions": {
"2306.0xc0004a1410.false": {
"1183.0xc000455050.false": {
"title": "false",
"type": "object"
},
"2340.0xc0004a1440.false": {
"1217.0xc000455080.false": {
"title": "false",
"type": "object"
},
@@ -3907,6 +3925,10 @@
"title": "Response",
"type": "object"
},
"controllers.AuthForm": {
"title": "AuthForm",
"type": "object"
},
"controllers.EmailForm": {
"title": "EmailForm",
"type": "object",
@@ -3931,108 +3953,15 @@
}
}
},
"controllers.RequestForm": {
"title": "RequestForm",
"type": "object",
"properties": {
"affiliation": {
"type": "string"
},
"application": {
"type": "string"
},
"autoSignin": {
"type": "boolean"
},
"captchaToken": {
"type": "string"
},
"captchaType": {
"type": "string"
},
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"code": {
"type": "string"
},
"countryCode": {
"type": "string"
},
"email": {
"type": "string"
},
"emailCode": {
"type": "string"
},
"firstName": {
"type": "string"
},
"idCard": {
"type": "string"
},
"lastName": {
"type": "string"
},
"method": {
"type": "string"
},
"name": {
"type": "string"
},
"organization": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"phoneCode": {
"type": "string"
},
"provider": {
"type": "string"
},
"redirectUri": {
"type": "string"
},
"region": {
"type": "string"
},
"relayState": {
"type": "string"
},
"samlRequest": {
"type": "string"
},
"samlResponse": {
"type": "string"
},
"state": {
"type": "string"
},
"type": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"controllers.Response": {
"title": "Response",
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/2306.0xc0004a1410.false"
"$ref": "#/definitions/1183.0xc000455050.false"
},
"data2": {
"$ref": "#/definitions/2340.0xc0004a1440.false"
"$ref": "#/definitions/1217.0xc000455080.false"
},
"msg": {
"type": "string"

View File

@@ -355,16 +355,28 @@ paths:
tags:
- Login API
operationId: ApiController.GetCaptcha
/api/api/get-captcha-status:
get:
tags:
- Token API
description: Get Login Error Counts
operationId: ApiController.GetCaptchaStatus
parameters:
- in: query
name: id
description: The id ( owner/name ) of user
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/get-webhook-event:
get:
tags:
- GetWebhookEventType API
operationId: ApiController.GetWebhookEventType
/api/api/get-captcha-status:
get:
tags:
- GetCaptchaStatus API
operationId: ApiController.GetCaptchaStatus
/api/api/reset-email-or-phone:
post:
tags:
@@ -429,7 +441,7 @@ paths:
/api/api/verify-code:
post:
tags:
- Account API
- Verification API
operationId: ApiController.VerifyCode
/api/api/webhook:
post:
@@ -1798,7 +1810,7 @@ paths:
description: Login information
required: true
schema:
$ref: '#/definitions/controllers.RequestForm'
$ref: '#/definitions/controllers.AuthForm'
responses:
"200":
description: The Response object
@@ -2543,10 +2555,10 @@ paths:
schema:
$ref: '#/definitions/Response'
definitions:
2306.0xc0004a1410.false:
1183.0xc000455050.false:
title: "false"
type: object
2340.0xc0004a1440.false:
1217.0xc000455080.false:
title: "false"
type: object
LaravelResponse:
@@ -2555,6 +2567,9 @@ definitions:
Response:
title: Response
type: object
controllers.AuthForm:
title: AuthForm
type: object
controllers.EmailForm:
title: EmailForm
type: object
@@ -2571,76 +2586,14 @@ definitions:
type: string
title:
type: string
controllers.RequestForm:
title: RequestForm
type: object
properties:
affiliation:
type: string
application:
type: string
autoSignin:
type: boolean
captchaToken:
type: string
captchaType:
type: string
clientId:
type: string
clientSecret:
type: string
code:
type: string
countryCode:
type: string
email:
type: string
emailCode:
type: string
firstName:
type: string
idCard:
type: string
lastName:
type: string
method:
type: string
name:
type: string
organization:
type: string
password:
type: string
phone:
type: string
phoneCode:
type: string
provider:
type: string
redirectUri:
type: string
region:
type: string
relayState:
type: string
samlRequest:
type: string
samlResponse:
type: string
state:
type: string
type:
type: string
username:
type: string
controllers.Response:
title: Response
type: object
properties:
data:
$ref: '#/definitions/2306.0xc0004a1410.false'
$ref: '#/definitions/1183.0xc000455050.false'
data2:
$ref: '#/definitions/2340.0xc0004a1440.false'
$ref: '#/definitions/1217.0xc000455080.false'
msg:
type: string
name:

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as AdapterBackend from "./backend/AdapterBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class AdapterListPage extends BaseListPage {
newAdapter() {

View File

@@ -21,7 +21,7 @@ import * as Setting from "./Setting";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ApplicationListPage extends BaseListPage {
constructor(props) {

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as CertBackend from "./backend/CertBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class CertListPage extends BaseListPage {
newCert() {

View File

@@ -13,8 +13,9 @@
// limitations under the License.
import React from "react";
import {Avatar, Input, List} from "antd";
import {Avatar, Input, List, Spin} from "antd";
import {CopyOutlined, DislikeOutlined, LikeOutlined, SendOutlined} from "@ant-design/icons";
import i18next from "i18next";
const {TextArea} = Input;
@@ -29,7 +30,7 @@ class ChatBox extends React.Component {
}
componentDidUpdate(prevProps) {
if (prevProps.messages !== this.props.messages) {
if (prevProps.messages !== this.props.messages && this.props.messages !== null) {
this.scrollToListItem(this.props.messages.length);
}
}
@@ -74,11 +75,19 @@ class ChatBox extends React.Component {
};
renderList() {
if (this.props.messages === undefined || this.props.messages === null) {
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "20%"}} />
</div>
);
}
return (
<div ref={this.listContainerRef} style={{position: "relative", maxHeight: "calc(100vh - 140px)", overflowY: "auto"}}>
<List
itemLayout="horizontal"
dataSource={this.props.messages === undefined ? undefined : [...this.props.messages, {}]}
dataSource={[...this.props.messages, {}]}
renderItem={(item, index) => {
if (Object.keys(item).length === 0 && item.constructor === Object) {
return <List.Item id={`chatbox-list-item-${index}`} style={{

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as ChatBackend from "./backend/ChatBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ChatListPage extends BaseListPage {
newChat() {

View File

@@ -13,8 +13,8 @@
// limitations under the License.
import React from "react";
import {Menu} from "antd";
import {LayoutOutlined} from "@ant-design/icons";
import {Button, Menu} from "antd";
import {DeleteOutlined, LayoutOutlined, PlusOutlined} from "@ant-design/icons";
class ChatMenu extends React.Component {
constructor(props) {
@@ -38,6 +38,7 @@ class ChatMenu extends React.Component {
categories[chat.category].push(chat);
});
const selectedKeys = this.state === undefined ? [] : this.state.selectedKeys;
return Object.keys(categories).map((category, index) => {
return {
key: `${index}`,
@@ -45,10 +46,50 @@ class ChatMenu extends React.Component {
label: category,
children: categories[category].map((chat, chatIndex) => {
const globalChatIndex = chats.indexOf(chat);
const isSelected = selectedKeys.includes(`${index}-${chatIndex}`);
return {
key: `${index}-${chatIndex}`,
index: globalChatIndex,
label: chat.displayName,
label: (
<div
className="menu-item-container"
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
{chat.displayName}
{isSelected && (
<DeleteOutlined
className="menu-item-delete-icon"
style={{
visibility: "visible",
color: "inherit",
transition: "color 0.3s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = "inherit";
}}
onMouseDown={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.4)";
}}
onMouseUp={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onClick={(e) => {
e.stopPropagation();
if (this.props.onDeleteChat) {
this.props.onDeleteChat(globalChatIndex);
}
}}
/>
)}
</div>
),
};
}),
};
@@ -60,8 +101,8 @@ class ChatMenu extends React.Component {
const selectedItem = this.chatsToItems(this.props.chats)[categoryIndex].children[chatIndex];
this.setState({selectedKeys: [`${categoryIndex}-${chatIndex}`]});
if (this.props.onSelect) {
this.props.onSelect(selectedItem.index);
if (this.props.onSelectChat) {
this.props.onSelectChat(selectedItem.index);
}
};
@@ -85,14 +126,40 @@ class ChatMenu extends React.Component {
const items = this.chatsToItems(this.props.chats);
return (
<Menu
mode="inline"
openKeys={this.state.openKeys}
selectedKeys={this.state.selectedKeys}
onOpenChange={this.onOpenChange}
onSelect={this.onSelect}
items={items}
/>
<>
<Button
icon={<PlusOutlined />}
style={{
width: "calc(100% - 8px)",
height: "40px",
margin: "4px",
borderColor: "rgb(229,229,229)",
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "rgba(89,54,213,0.6)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = "rgba(0, 0, 0, 0.1)";
}}
onMouseDown={(e) => {
e.currentTarget.style.borderColor = "rgba(89,54,213,0.4)";
}}
onMouseUp={(e) => {
e.currentTarget.style.borderColor = "rgba(89,54,213,0.6)";
}}
onClick={this.props.onAddChat}
>
New Chat
</Button>
<Menu
mode="inline"
openKeys={this.state.openKeys}
selectedKeys={this.state.selectedKeys}
onOpenChange={this.onOpenChange}
onSelect={this.onSelect}
items={items}
/>
</>
);
}
}

View File

@@ -24,7 +24,7 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ChatPage extends BaseListPage {
newChat() {
newChat(chat) {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.applicationName,
@@ -33,8 +33,8 @@ class ChatPage extends BaseListPage {
updatedTime: moment().format(),
organization: this.props.account.owner,
displayName: `New Chat - ${randomName}`,
type: "Single",
category: "Chat Category - 1",
type: "AI",
category: chat !== undefined ? chat.category : "Chat Category - 1",
user1: `${this.props.account.owner}/${this.props.account.name}`,
user2: "",
users: [`${this.props.account.owner}/${this.props.account.name}`],
@@ -42,40 +42,6 @@ class ChatPage extends BaseListPage {
};
}
// addChat() {
// const newChat = this.newChat();
// ChatBackend.addChat(newChat)
// .then((res) => {
// if (res.status === "ok") {
// this.props.history.push({pathname: `/chats/${newChat.name}`, mode: "add"});
// Setting.showMessage("success", i18next.t("general:Successfully added"));
// } else {
// Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
// }
// })
// .catch(error => {
// Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
// });
// }
//
// deleteChat(i) {
// ChatBackend.deleteChat(this.state.data[i])
// .then((res) => {
// if (res.status === "ok") {
// Setting.showMessage("success", i18next.t("general:Successfully deleted"));
// this.setState({
// data: Setting.deleteRow(this.state.data, i),
// pagination: {total: this.state.pagination.total - 1},
// });
// } else {
// Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
// }
// })
// .catch(error => {
// Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
// });
// }
newMessage(text) {
const randomName = Setting.getRandomName();
return {
@@ -115,39 +81,116 @@ class ChatPage extends BaseListPage {
});
}
addChat(chat) {
const newChat = this.newChat(chat);
ChatBackend.addChat(newChat)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully added"));
this.setState({
chatName: newChat.name,
messages: null,
});
this.getMessages(newChat.name);
const {pagination} = this.state;
this.fetch({pagination});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteChat(chats, i, chat) {
ChatBackend.deleteChat(chat)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
const data = Setting.deleteRow(this.state.data, i);
const j = Math.min(i, data.length - 1);
if (j < 0) {
this.setState({
chatName: undefined,
messages: undefined,
data: data,
});
} else {
const focusedChat = data[j];
this.setState({
chatName: focusedChat.name,
messages: null,
data: data,
});
this.getMessages(focusedChat.name);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
renderTable(chats) {
return (this.state.loading) ? <Spin size="large" style={{marginLeft: "50%", marginTop: "10%"}} /> : (
(
<div style={{display: "flex", height: "calc(100vh - 140px)"}}>
<div style={{width: "250px", height: "100%", backgroundColor: "white", borderRight: "1px solid rgb(245,245,245)"}}>
<ChatMenu chats={chats} onSelect={(i) => {
const chat = chats[i];
this.getMessages(chat.name);
this.setState({
chatName: chat.name,
});
}} />
</div>
<div style={{flex: 1, height: "100%", backgroundColor: "white", position: "relative"}}>
<div style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: "url(https://cdn.casbin.org/img/casdoor-logo_1185x256.png)",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "200px auto",
backgroundBlendMode: "luminosity",
filter: "grayscale(80%) brightness(140%) contrast(90%)",
opacity: 0.5,
}}>
</div>
<ChatBox messages={this.state.messages} sendMessage={(text) => {this.sendMessage(text);}} account={this.props.account} />
</div>
const onSelectChat = (i) => {
const chat = chats[i];
this.setState({
chatName: chat.name,
messages: null,
});
this.getMessages(chat.name);
};
const onAddChat = () => {
const chat = this.state.data.filter(chat => chat.name === this.state.chatName)[0];
this.addChat(chat);
};
const onDeleteChat = (i) => {
const chat = chats[i];
this.deleteChat(chats, i, chat);
};
if (this.state.loading) {
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "10%"}} />
</div>
)
);
}
return (
<div style={{display: "flex", height: "calc(100vh - 140px)"}}>
<div style={{width: "250px", height: "100%", backgroundColor: "white", borderRight: "1px solid rgb(245,245,245)"}}>
<ChatMenu chats={chats} onSelectChat={onSelectChat} onAddChat={onAddChat} onDeleteChat={onDeleteChat} />
</div>
<div style={{flex: 1, height: "100%", backgroundColor: "white", position: "relative"}}>
{
this.state.messages === null ? null : (
<div style={{
position: "absolute",
top: -50,
left: 0,
right: 0,
bottom: 0,
backgroundImage: "url(https://cdn.casbin.org/img/casdoor-logo_1185x256.png)",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "200px auto",
backgroundBlendMode: "luminosity",
filter: "grayscale(80%) brightness(140%) contrast(90%)",
opacity: 0.5,
}}>
</div>
)
}
<ChatBox messages={this.state.messages} sendMessage={(text) => {this.sendMessage(text);}} account={this.props.account} />
</div>
</div>
);
}

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as MessageBackend from "./backend/MessageBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class MessageListPage extends BaseListPage {
newMessage() {

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as ModelBackend from "./backend/ModelBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ModelListPage extends BaseListPage {
newModel() {

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class OrganizationListPage extends BaseListPage {
newOrganization() {

View File

@@ -21,7 +21,7 @@ import * as PaymentBackend from "./backend/PaymentBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import * as Provider from "./auth/Provider";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class PaymentListPage extends BaseListPage {
newPayment() {

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as PermissionBackend from "./backend/PermissionBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class PermissionListPage extends BaseListPage {
newPermission() {

View File

@@ -21,7 +21,7 @@ import * as ProductBackend from "./backend/ProductBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import {EditOutlined} from "@ant-design/icons";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ProductListPage extends BaseListPage {
newProduct() {

View File

@@ -125,6 +125,8 @@ class ProviderEditPage extends React.Component {
} else {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
}
case "AI":
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
@@ -278,17 +280,20 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Alipay");
} else if (value === "Captcha") {
this.updateProviderField("type", "Default");
} else if (value === "AI") {
this.updateProviderField("type", "OpenAI API - GPT");
}
})}>
{
[
{id: "OAuth", name: "OAuth"},
{id: "AI", name: "AI"},
{id: "Captcha", name: "Captcha"},
{id: "Email", name: "Email"},
{id: "OAuth", name: "OAuth"},
{id: "Payment", name: "Payment"},
{id: "SAML", name: "SAML"},
{id: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"},
{id: "SAML", name: "SAML"},
{id: "Payment", name: "Payment"},
{id: "Captcha", name: "Captcha"},
]
.sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
@@ -437,16 +442,20 @@ class ProviderEditPage extends React.Component {
{
this.state.provider.category === "Captcha" && this.state.provider.type === "Default" ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel(this.state.provider)}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField("clientId", e.target.value);
}} />
</Col>
</Row>
{
this.state.provider.category === "AI" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel(this.state.provider)}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField("clientId", e.target.value);
}} />
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecretLabel(this.state.provider)}

View File

@@ -21,7 +21,7 @@ import * as ProviderBackend from "./backend/ProviderBackend";
import * as Provider from "./auth/Provider";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ProviderListPage extends BaseListPage {
constructor(props) {

View File

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

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as RoleBackend from "./backend/RoleBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class RoleListPage extends BaseListPage {
newRole() {

View File

@@ -19,7 +19,7 @@ import {Link} from "react-router-dom";
import {Table, Tag} from "antd";
import React from "react";
import * as SessionBackend from "./backend/SessionBackend";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class SessionListPage extends BaseListPage {

View File

@@ -204,6 +204,12 @@ export const OtherProviderInfo = {
url: "https://www.cloudflare.com/products/turnstile/",
},
},
AI: {
"OpenAI API - GPT": {
logo: `${StaticBaseUrl}/img/social_openai.svg`,
url: "https://platform.openai.com",
},
},
};
export function initCountries() {
@@ -855,6 +861,10 @@ export function getProviderTypeOptions(category) {
{id: "GEETEST", name: "GEETEST"},
{id: "Cloudflare Turnstile", name: "Cloudflare Turnstile"},
]);
} else if (category === "AI") {
return ([
{id: "OpenAI API - GPT", name: "OpenAI API - GPT"},
]);
} else {
return [];
}

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as SyncerBackend from "./backend/SyncerBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class SyncerListPage extends BaseListPage {
newSyncer() {

View File

@@ -17,6 +17,7 @@ import * as SystemBackend from "./backend/SystemInfo";
import React from "react";
import * as Setting from "./Setting";
import i18next from "i18next";
import PrometheusInfoTable from "./table/PrometheusInfoTable";
class SystemInfo extends React.Component {
@@ -25,6 +26,7 @@ class SystemInfo extends React.Component {
this.state = {
systemInfo: {cpuUsage: [], memoryUsed: 0, memoryTotal: 0},
versionInfo: {},
prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
intervalId: null,
loading: true,
};
@@ -45,6 +47,11 @@ class SystemInfo extends React.Component {
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);
});
SystemBackend.getPrometheusInfo().then(res => {
this.setState({
prometheusInfo: res.data,
});
});
}, 1000 * 2);
this.setState({intervalId: id});
}).catch(error => {
@@ -80,7 +87,10 @@ class SystemInfo extends React.Component {
<br /> <br />
<Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} />
</div>;
const latencyUi = this.state.prometheusInfo.apiLatency.length <= 0 ? <Spin size="large" /> :
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"latency"} />;
const throughputUi = this.state.prometheusInfo.apiLatency.length <= 0 ? <Spin size="large" /> :
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"throughput"} />;
const link = this.state.versionInfo?.version !== "" ? `https://github.com/casdoor/casdoor/releases/tag/${this.state.versionInfo?.version}` : "";
let versionText = this.state.versionInfo?.version !== "" ? this.state.versionInfo?.version : i18next.t("system:Unknown version");
if (this.state.versionInfo?.commitOffset > 0) {
@@ -103,6 +113,16 @@ class SystemInfo extends React.Component {
{this.state.loading ? <Spin size="large" /> : memUi}
</Card>
</Col>
<Col span={24}>
<Card title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}>
{this.state.loading ? <Spin size="large" /> : latencyUi}
</Card>
</Col>
<Col span={24}>
<Card title={i18next.t("system:API Throughput")} bordered={true} style={{textAlign: "center", height: "100%"}}>
{this.state.loading ? <Spin size="large" /> : throughputUi}
</Card>
</Col>
</Row>
<Divider />
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as TokenBackend from "./backend/TokenBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class TokenListPage extends BaseListPage {
newToken() {

View File

@@ -22,7 +22,7 @@ import * as Setting from "./Setting";
import * as UserBackend from "./backend/UserBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class UserListPage extends BaseListPage {
constructor(props) {

View File

@@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as WebhookBackend from "./backend/WebhookBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class WebhookListPage extends BaseListPage {
newWebhook() {

View File

@@ -66,7 +66,7 @@ class SignupPage extends React.Component {
super(props);
this.state = {
classes: props,
applicationName: props.match?.params?.applicationName ?? null,
applicationName: (props.applicationName ?? props.match?.params?.applicationName) ?? null,
email: "",
phone: "",
countryCode: "",

View File

@@ -33,3 +33,13 @@ export function getVersionInfo() {
},
}).then(res => res.json());
}
export function getPrometheusInfo() {
return fetch(`${Setting.ServerUrl}/api/get-prometheus-info `, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -113,9 +113,9 @@ export function setPassword(userOwner, userName, oldPassword, newPassword, code
}).then(res => res.json());
}
export function sendCode(checkType, captchaToken, clientSecret, method, countryCode = "", dest, type, applicationId, checkUser = "") {
export function sendCode(captchaType, captchaToken, clientSecret, method, countryCode = "", dest, type, applicationId, checkUser = "") {
const formData = new FormData();
formData.append("checkType", checkType);
formData.append("captchaType", captchaType);
formData.append("captchaToken", captchaToken);
formData.append("clientSecret", clientSecret);
formData.append("method", method);

View File

@@ -18,7 +18,7 @@ import * as Setting from "../Setting";
import i18next from "i18next";
import * as LdapBackend from "../backend/LdapBackend";
import {Link} from "react-router-dom";
import PopconfirmModal from "../PopconfirmModal";
import PopconfirmModal from "../common/modal/PopconfirmModal";
class LdapTable extends React.Component {
constructor(props) {

View File

@@ -0,0 +1,82 @@
// 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 {Table} from "antd";
class PrometheusInfoTable extends React.Component {
constructor(props) {
super(props);
this.state = {
table: props.table,
};
}
render() {
const latencyColumns = [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Method",
dataIndex: "method",
key: "method",
},
{
title: "Count",
dataIndex: "count",
key: "count",
},
{
title: "Latency(ms)",
dataIndex: "latency",
key: "latency",
},
];
const throughputColumns = [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Method",
dataIndex: "method",
key: "method",
},
{
title: "Throughput",
dataIndex: "throughput",
key: "throughput",
},
];
if (this.state.table === "latency") {
return (
<div style={{height: "300px", overflow: "auto"}}>
<Table columns={latencyColumns} dataSource={this.props.prometheusInfo.apiLatency} pagination={false} />
</div>
);
} else if (this.state.table === "throughput") {
return (
<div style={{height: "300px", overflow: "auto"}}>
Total Throughput: {this.props.prometheusInfo.totalThroughput}
<Table columns={throughputColumns} dataSource={this.props.prometheusInfo.apiThroughput} pagination={false} />
</div>
);
}
}
}
export default PrometheusInfoTable;