Compare commits

...

56 Commits

Author SHA1 Message Date
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
511aefb706 Disable faulty Beego filter 2023-04-25 20:02:13 +08:00
1003639e5b feat: support for prometheus (#1784) 2023-04-25 16:06:09 +08:00
fe53e90d37 fix: signup page of the app-built-in failed to load (#1785) 2023-04-25 16:00:24 +08:00
8c73cb5395 fix: fix golangci-lint (#1775) 2023-04-23 17:02:29 +08:00
06ebc04032 Can add/delete chat 2023-04-23 01:19:44 +08:00
0ee98e2582 Add loading to chat box 2023-04-23 00:25:09 +08:00
d25508fa56 Improve chat UI 2023-04-22 23:20:40 +08:00
916a55b633 fix: fixed failed to update information when name duplicate (#1773)
* fix: fixed failed to update information when name duplicate

* fix: Use GetOwnerAndNameFromId and GetId functions instead of split

* Update organization.go

* Update role.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-04-22 21:15:06 +08:00
a6c7b95f97 fix: fixed rows duplicates after sort by column (#1772) 2023-04-22 20:18:38 +08:00
4f8dd771bc feat: fix bug that can not get application in signup/oauth/ router (#1766) 2023-04-22 18:20:45 +08:00
e0028f5eed fix: add more events to webhooks (#1771) 2023-04-22 17:11:28 +08:00
6d6cbc7e6f feat: add dynamic mode for provider to enable verification code when the login password is wrong (#1753)
* fix: update webAuthnBufferDecode to support Base64URL for WebAuthn updates

* feat: enable verification code when the login password is wrong

* fix: only enable captcha when login in password

* fix: disable login error limits when captcha on

* fix: pass "enableCaptcha" as an optional param

* fix: change enbleCapctah to optional bool param
2023-04-22 16:16:25 +08:00
ee8c2650c3 Remove useless "/api/login/oauth/code" API and update Swagger 2023-04-22 09:47:52 +08:00
f3ea39d20c Fix result page button link 2023-04-21 23:56:33 +08:00
e78d9e5d2b Fix local file system storage provider path error 2023-04-21 10:12:09 +08:00
19209718ea feat: fix wrong CAS login mode (#1762) 2023-04-20 22:18:02 +08:00
e75d26260a Fix table name in getEnforcer() 2023-04-20 01:33:47 +08:00
6572ab69ce fix: fix pemContent decode error bug for WeChat Pay provider (#1751) 2023-04-19 22:13:13 +08:00
8db87a7559 fix: function comments (#1757)
Modify the function annotation so that the swagger can parse correctly
2023-04-19 21:19:48 +08:00
0dcccfc19c feat: rollback anted to v5.2.3 (#1755) 2023-04-19 11:30:49 +08:00
96219442f5 feat: fix Tencent Cloud OSS storage connect incorrect issue (#1752)
* fix: fix Tencent Cloud OSS storage connect incorrect

* Update provider.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-04-18 21:30:46 +08:00
903745c540 fix: improve LDAP page UI (#1749)
* refactor: improve LDAP sync page

* refactor: update anted version

* chore: i18
2023-04-17 22:03:05 +08:00
df741805cd Fix chat send 2023-04-17 20:50:03 +08:00
ee5c3f3f39 feat: fix display name null error during 3rd-party binding (#1747) 2023-04-17 15:39:33 +08:00
714f69be7b Use HTTP for IP host in getOriginFromHost() 2023-04-17 00:55:40 +08:00
0d12972e92 Fix "auto single OAuth signin doesn't work" bug 2023-04-17 00:38:48 +08:00
78b62c28ab Fix the wrong order of g policy in enforce() API 2023-04-16 22:26:22 +08:00
wht
5c26335fd6 feat: add rule option for phone in application's signup page (#1745) 2023-04-16 20:34:06 +08:00
7edaeafea5 Call refreshAvatar() in addUser() 2023-04-16 01:00:02 +08:00
336f3f7a7b Add user.refreshAvatar() 2023-04-16 01:00:02 +08:00
47dc3715f9 feat: handle error when parsing samlResponse (#1744)
* fix: handle err from parse samlResponse

* fix: lint
2023-04-16 00:36:25 +08:00
7503e05a4a Improve menu style 2023-04-15 18:08:21 +08:00
b89cf1de07 Add karma to account items 2023-04-15 16:05:33 +08:00
be87078c25 Fix vi i18n 2023-04-15 14:16:49 +08:00
faf352acc5 Fix i18n 2023-04-15 11:17:31 +08:00
0db61dd658 Add empty list item and expand menu by default 2023-04-15 10:54:56 +08:00
ebe8ad8669 Improve UI effect 2023-04-15 10:54:56 +08:00
2e01f0d10e Add input box 2023-04-15 10:54:55 +08:00
754fa1e745 Add chat box 2023-04-15 10:54:55 +08:00
8b9e0ba96b Add chat page 2023-04-15 10:54:55 +08:00
b0656aca36 Add chat and message pages 2023-04-15 10:54:54 +08:00
623b4fee17 feat: pre-ensure tempFiles folder exists before uploading files (#1739)
When deployed with docker, the user `casdoor` has no permission to mkdir `tempFiles`, so let's create the folder first.
2023-04-14 19:14:59 +08:00
1b1de1dd01 feat: add LDAP custom filter support (#1719)
* refactor: improve ldap server code

* feat: custom filter

* fix: fix displayName mapping

* feat: add custom filter search fields

* chore: add license

* chore: i18n

* chore: i18n

* chore: update init field
2023-04-13 14:12:31 +08:00
968d8646b2 fix: update webAuthnBufferDecode to support Base64URL for WebAuthn updates (#1734) 2023-04-12 21:33:54 +08:00
94eef7dceb feat: fix adapter set organizations invalid bug (#1729) 2023-04-11 22:38:00 +08:00
fe647939ce fix: fix CAS callback url not match bug (#1728)
Co-authored-by: mfk <mfk@hengwei.com.cn>
2023-04-11 19:26:57 +08:00
984a69cb4b feat: fix wrong Vietnamese flag (#1724)
* fix wrong Vietnam country code

* fix wrong Vietnam country code

* fix wrong Vietnam country code

* fix wrong Vietnam country code
2023-04-10 22:42:12 +08:00
098a1ece68 fix: rollback the version of webauthn in go mod to fix "atob" bug (#1721) 2023-04-10 20:14:27 +08:00
ad6f2ad2e1 feat: add wechatpay support. (#1710)
* feat: add wechatpay support.

* feat: add wechatpay support.

* Update wechatv3pay.go

* fix: update format.

* Update wechatv3pay.go

* Update wechatv3pay.go

* Update wechatv3pay.go

* fix: update file format.

* fix: improve the front of wechat payment.

* fix: change clientId2 to clientId.

* fix: fix the code format.

* fix: return backend error information to frontend.
2023-04-10 18:04:10 +08:00
2d55252261 Add chat and message pages 2023-04-09 15:54:22 +08:00
30ea3a1335 Improve getTags() 2023-04-09 15:54:21 +08:00
b7d78d1e27 fix: validate parameter and nil in func updateUser (#1714)
* fix: validate parameter and nil in func updateUser

* fix: delete blank line
2023-04-09 10:35:30 +08:00
3d5a645a3b feat: fix field name error of termsOfUse (#1715) 2023-04-09 01:01:04 +08:00
4ad21e7781 fix: fix WeCom provider method 2023-04-07 01:10:46 +08:00
b99a0c3ca2 feat: optimize the "forget password" page (#1709) 2023-04-06 23:06:18 +08:00
131 changed files with 6328 additions and 1711 deletions

View File

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

View File

@ -64,6 +64,7 @@ COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf
COPY --from=BACK /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt COPY --from=BACK /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
COPY --from=FRONT /web/build ./web/build COPY --from=FRONT /web/build ./web/build
RUN mkdir tempFiles
ENTRYPOINT ["/bin/bash"] ENTRYPOINT ["/bin/bash"]
CMD ["/docker-entrypoint.sh"] CMD ["/docker-entrypoint.sh"]

View File

@ -90,6 +90,7 @@ p, *, *, GET, /api/userinfo, *, *
p, *, *, GET, /api/user, *, * p, *, *, GET, /api/user, *, *
p, *, *, POST, /api/webhook, *, * p, *, *, POST, /api/webhook, *, *
p, *, *, GET, /api/get-webhook-event, *, * p, *, *, GET, /api/get-webhook-event, *, *
p, *, *, GET, /api/get-captcha-status, *, *
p, *, *, *, /api/login/oauth, *, * p, *, *, *, /api/login/oauth, *, *
p, *, *, GET, /api/get-application, *, * p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-organization-applications, *, * p, *, *, GET, /api/get-organization-applications, *, *
@ -108,6 +109,7 @@ p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, * p, *, *, POST, /api/send-verification-code, *, *
p, *, *, GET, /api/get-captcha, *, * p, *, *, GET, /api/get-captcha, *, *
p, *, *, POST, /api/verify-captcha, *, * p, *, *, POST, /api/verify-captcha, *, *
p, *, *, POST, /api/verify-code, *, *
p, *, *, POST, /api/reset-email-or-phone, *, * p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, * p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, * p, *, *, GET, /.well-known/openid-configuration, *, *
@ -119,6 +121,8 @@ p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, * p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, * p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, * p, *, *, GET, /api/get-default-application, *, *
p, *, *, GET, /api/get-prometheus-info, *, *
p, *, *, *, /api/metrics, *, *
` `
sa := stringadapter.NewAdapter(ruleText) sa := stringadapter.NewAdapter(ruleText)

View File

@ -21,6 +21,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -34,44 +35,6 @@ const (
ResponseTypeCas = "cas" 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 { type Response struct {
Status string `json:"status"` Status string `json:"status"`
Msg string `json:"msg"` Msg string `json:"msg"`
@ -108,28 +71,28 @@ func (c *ApiController) Signup() {
return return
} }
var form RequestForm var authForm form.AuthForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form) err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if !application.EnableSignUp { if !application.EnableSignUp {
c.ResponseError(c.T("account:The application does not allow to sign up new account")) c.ResponseError(c.T("account:The application does not allow to sign up new account"))
return return
} }
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", form.Organization)) organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", authForm.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()) msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
if msg != "" { if msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
return return
} }
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && form.Email != "" { if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
checkResult := object.CheckVerificationCode(form.Email, form.EmailCode, c.GetAcceptLanguage()) checkResult := object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
if checkResult.Code != object.VerificationSuccess { if checkResult.Code != object.VerificationSuccess {
c.ResponseError(checkResult.Msg) c.ResponseError(checkResult.Msg)
return return
@ -137,9 +100,9 @@ func (c *ApiController) Signup() {
} }
var checkPhone string var checkPhone string
if application.IsSignupItemVisible("Phone") && form.Phone != "" { if application.IsSignupItemVisible("Phone") && application.GetSignupItemRule("Phone") != "No verification" && authForm.Phone != "" {
checkPhone, _ = util.GetE164Number(form.Phone, form.CountryCode) checkPhone, _ = util.GetE164Number(authForm.Phone, authForm.CountryCode)
checkResult := object.CheckVerificationCode(checkPhone, form.PhoneCode, c.GetAcceptLanguage()) checkResult := object.CheckVerificationCode(checkPhone, authForm.PhoneCode, c.GetAcceptLanguage())
if checkResult.Code != object.VerificationSuccess { if checkResult.Code != object.VerificationSuccess {
c.ResponseError(checkResult.Msg) c.ResponseError(checkResult.Msg)
return return
@ -148,7 +111,7 @@ func (c *ApiController) Signup() {
id := util.GenerateId() id := util.GenerateId()
if application.GetSignupItemRule("ID") == "Incremental" { if application.GetSignupItemRule("ID") == "Incremental" {
lastUser := object.GetLastUser(form.Organization) lastUser := object.GetLastUser(authForm.Organization)
lastIdInt := -1 lastIdInt := -1
if lastUser != nil { if lastUser != nil {
@ -158,7 +121,7 @@ func (c *ApiController) Signup() {
id = strconv.Itoa(lastIdInt + 1) id = strconv.Itoa(lastIdInt + 1)
} }
username := form.Username username := authForm.Username
if !application.IsSignupItemVisible("Username") { if !application.IsSignupItemVisible("Username") {
username = id username = id
} }
@ -170,21 +133,21 @@ func (c *ApiController) Signup() {
} }
user := &object.User{ user := &object.User{
Owner: form.Organization, Owner: authForm.Organization,
Name: username, Name: username,
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
Id: id, Id: id,
Type: "normal-user", Type: "normal-user",
Password: form.Password, Password: authForm.Password,
DisplayName: form.Name, DisplayName: authForm.Name,
Avatar: organization.DefaultAvatar, Avatar: organization.DefaultAvatar,
Email: form.Email, Email: authForm.Email,
Phone: form.Phone, Phone: authForm.Phone,
CountryCode: form.CountryCode, CountryCode: authForm.CountryCode,
Address: []string{}, Address: []string{},
Affiliation: form.Affiliation, Affiliation: authForm.Affiliation,
IdCard: form.IdCard, IdCard: authForm.IdCard,
Region: form.Region, Region: authForm.Region,
Score: initScore, Score: initScore,
IsAdmin: false, IsAdmin: false,
IsGlobalAdmin: false, IsGlobalAdmin: false,
@ -203,10 +166,10 @@ func (c *ApiController) Signup() {
} }
if application.GetSignupItemRule("Display name") == "First, last" { if application.GetSignupItemRule("Display name") == "First, last" {
if form.FirstName != "" || form.LastName != "" { if authForm.FirstName != "" || authForm.LastName != "" {
user.DisplayName = fmt.Sprintf("%s %s", form.FirstName, form.LastName) user.DisplayName = fmt.Sprintf("%s %s", authForm.FirstName, authForm.LastName)
user.FirstName = form.FirstName user.FirstName = authForm.FirstName
user.LastName = form.LastName user.LastName = authForm.LastName
} }
} }
@ -223,7 +186,7 @@ func (c *ApiController) Signup() {
c.SetSessionUsername(user.GetId()) c.SetSessionUsername(user.GetId())
} }
object.DisableVerificationCode(form.Email) object.DisableVerificationCode(authForm.Email)
object.DisableVerificationCode(checkPhone) object.DisableVerificationCode(checkPhone)
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)

View File

@ -28,6 +28,7 @@ import (
"github.com/casdoor/casdoor/captcha" "github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/idp" "github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
@ -56,7 +57,7 @@ func tokenToResponse(token *object.Token) *Response {
} }
// HandleLoggedIn ... // 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() userId := user.GetId()
allowed, err := object.CheckAccessPermission(userId, application) allowed, err := object.CheckAccessPermission(userId, application)
@ -221,21 +222,21 @@ func isProxyProviderType(providerType string) bool {
// @Param nonce query string false nonce // @Param nonce query string false nonce
// @Param code_challenge_method query string false code_challenge_method // @Param code_challenge_method query string false code_challenge_method
// @Param code_challenge query string false code_challenge // @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 // @Success 200 {object} Response The Response object
// @router /login [post] // @router /login [post]
func (c *ApiController) Login() { func (c *ApiController) Login() {
resp := &Response{} resp := &Response{}
var form RequestForm var authForm form.AuthForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form) err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if form.Username != "" { if authForm.Username != "" {
if form.Type == ResponseTypeLogin { if authForm.Type == ResponseTypeLogin {
if c.GetSessionUsername() != "" { if c.GetSessionUsername() != "" {
c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername()) c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername())
return return
@ -245,43 +246,25 @@ func (c *ApiController) Login() {
var user *object.User var user *object.User
var msg string var msg string
if form.Password == "" { if authForm.Password == "" {
var verificationCodeType string if user = object.GetUserByFields(authForm.Organization, authForm.Username); user == nil {
var checkResult string c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(authForm.Organization, authForm.Username)))
if form.Name != "" {
user = object.GetUserByFields(form.Organization, form.Name)
}
// check result through Email or Phone
var checkDest string
if strings.Contains(form.Username, "@") {
verificationCodeType = "email"
if user != nil && util.GetMaskedEmail(user.Email) == form.Username {
form.Username = user.Email
}
checkDest = form.Username
} else {
verificationCodeType = "phone"
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.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)))
return return
} }
if verificationCodeType == "phone" {
form.CountryCode = user.GetCountryCode(form.CountryCode) verificationCodeType := object.GetVerifyType(authForm.Username)
var checkDest string
if verificationCodeType == object.VerifyTypePhone {
authForm.CountryCode = user.GetCountryCode(authForm.CountryCode)
var ok bool var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok { 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"), form.CountryCode)) c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), authForm.CountryCode))
return return
} }
} }
checkResult = object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage()) // check result through Email or Phone
checkResult := object.CheckSigninCode(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 { if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult)) c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
return return
@ -290,18 +273,18 @@ func (c *ApiController) Login() {
// disable the verification code // disable the verification code
object.DisableVerificationCode(checkDest) object.DisableVerificationCode(checkDest)
} else { } else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil { 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 return
} }
if !application.EnablePassword { if !application.EnablePassword {
c.ResponseError(c.T("auth:The login method: login with password is not enabled for the application")) c.ResponseError(c.T("auth:The login method: login with password is not enabled for the application"))
return return
} }
var enableCaptcha bool
if object.CheckToEnableCaptcha(application) { if enableCaptcha = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); enableCaptcha {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(form.CaptchaType, form.CaptchaToken, form.ClientSecret) isHuman, err := captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -313,41 +296,42 @@ func (c *ApiController) Login() {
} }
} }
password := form.Password password := authForm.Password
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage()) user, msg = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
} }
if msg != "" { if msg != "" {
resp = &Response{Status: "error", Msg: msg} resp = &Response{Status: "error", Msg: msg}
} else { } else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil { 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 return
} }
resp = c.HandleLoggedIn(application, user, &form) resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) }) util.SafeGoroutine(func() { object.AddRecord(record) })
} }
} else if form.Provider != "" { } else if authForm.Provider != "" {
var application *object.Application var application *object.Application
if form.ClientId != "" { if authForm.ClientId != "" {
application = object.GetApplicationByClientId(form.ClientId) application = object.GetApplicationByClientId(authForm.ClientId)
} else { } else {
application = object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) application = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
} }
if application == nil { 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 return
} }
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", application.Organization)) 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) providerItem := application.GetProviderItem(provider.Name)
if !providerItem.IsProviderVisible() { if !providerItem.IsProviderVisible() {
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s is not enabled for the application"), provider.Name)) c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s is not enabled for the application"), provider.Name))
@ -357,7 +341,7 @@ func (c *ApiController) Login() {
userInfo := &idp.UserInfo{} userInfo := &idp.UserInfo{}
if provider.Category == "SAML" { if provider.Category == "SAML" {
// SAML // SAML
userInfo.Id, err = object.ParseSamlResponse(form.SamlResponse, provider.Type) userInfo.Id, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -372,7 +356,7 @@ func (c *ApiController) Login() {
clientSecret = provider.ClientSecret2 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 { if idProvider == nil {
c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type)) c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type))
return return
@ -380,13 +364,13 @@ func (c *ApiController) Login() {
setHttpClient(idProvider, provider.Type) setHttpClient(idProvider, provider.Type)
if form.State != conf.GetConfigString("authState") && form.State != application.Name { 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"), form.State)) c.ResponseError(fmt.Sprintf(c.T("auth:State expected: %s, but got: %s"), conf.GetConfigString("authState"), authForm.State))
return return
} }
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338 // https://github.com/golang/oauth2/issues/123#issuecomment-103715338
token, err := idProvider.GetToken(form.Code) token, err := idProvider.GetToken(authForm.Code)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -404,7 +388,7 @@ func (c *ApiController) Login() {
} }
} }
if form.Method == "signup" { if authForm.Method == "signup" {
user := &object.User{} user := &object.User{}
if provider.Category == "SAML" { if provider.Category == "SAML" {
user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id)) user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id))
@ -419,7 +403,7 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator")) 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 := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
@ -494,7 +478,7 @@ func (c *ApiController) Login() {
object.SetUserOAuthProperties(organization, user, provider.Type, userInfo) object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
object.LinkUserAccount(user, provider.Type, userInfo.Id) 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 := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
@ -510,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: "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} // resp = &Response{Status: "ok", Msg: "", Data: res}
} else { // form.Method != "signup" } else { // authForm.Method != "signup"
userId := c.GetSessionUsername() userId := c.GetSessionUsername()
if userId == "" { if userId == "" {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id)), userInfo) c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id)), userInfo)
@ -538,21 +522,21 @@ func (c *ApiController) Login() {
} else { } else {
if c.GetSessionUsername() != "" { if c.GetSessionUsername() != "" {
// user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in // 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 { 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 return
} }
user := c.getCurrentUser() user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &form) resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) }) util.SafeGoroutine(func() { object.AddRecord(record) })
} else { } 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 return
} }
} }
@ -564,7 +548,7 @@ func (c *ApiController) Login() {
func (c *ApiController) GetSamlLogin() { func (c *ApiController) GetSamlLogin() {
providerId := c.Input().Get("id") providerId := c.Input().Get("id")
relayState := c.Input().Get("relayState") relayState := c.Input().Get("relayState")
authURL, method, err := object.GenerateSamlLoginUrl(providerId, relayState, c.GetAcceptLanguage()) authURL, method, err := object.GenerateSamlRequest(providerId, relayState, c.Ctx.Request.Host, c.GetAcceptLanguage())
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} }
@ -628,3 +612,21 @@ func (c *ApiController) GetWebhookEventType() {
wechatScanType = "" wechatScanType = ""
c.ServeJSON() c.ServeJSON()
} }
// GetCaptchaStatus
// @Title GetCaptchaStatus
// @Tag Token API
// @Description Get Login Error Counts
// @Param id query string true "The id ( owner/name ) of user"
// @Success 200 {object} controllers.Response The Response object
// @router /api/get-captcha-status [get]
func (c *ApiController) GetCaptchaStatus() {
organization := c.Input().Get("organization")
userId := c.Input().Get("user_id")
user := object.GetUserByFields(organization, userId)
var captchaEnabled bool
if user != nil && user.SigninWrongTimes >= object.SigninWrongTimesLimit {
captchaEnabled = true
}
c.ResponseOk(captchaEnabled)
}

View File

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

View File

@ -72,6 +72,11 @@ func (c *RootController) CasProxyValidate() {
c.CasP3ServiceAndProxyValidate() c.CasP3ServiceAndProxyValidate()
} }
func queryUnescape(service string) string {
s, _ := url.QueryUnescape(service)
return s
}
func (c *RootController) CasP3ServiceAndProxyValidate() { func (c *RootController) CasP3ServiceAndProxyValidate() {
ticket := c.Input().Get("ticket") ticket := c.Input().Get("ticket")
format := c.Input().Get("format") format := c.Input().Get("format")
@ -91,7 +96,7 @@ func (c *RootController) CasP3ServiceAndProxyValidate() {
// find the token // find the token
if ok { if ok {
// check whether service is the one for which we previously issued token // check whether service is the one for which we previously issued token
if strings.HasPrefix(service, issuedService) { if strings.HasPrefix(service, issuedService) || strings.HasPrefix(queryUnescape(service), issuedService) {
serviceResponse.Success = response serviceResponse.Success = response
} else { } else {
// service not match // service not match

123
controllers/chat.go Normal file
View File

@ -0,0 +1,123 @@
// 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/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetChats
// @Title GetChats
// @Tag Chat API
// @Description get chats
// @Param owner query string true "The owner of chats"
// @Success 200 {array} object.Chat The Response object
// @router /get-chats [get]
func (c *ApiController) GetChats() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedChats(object.GetChats(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetChatCount(owner, field, value)))
chats := object.GetMaskedChats(object.GetPaginationChats(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(chats, paginator.Nums())
}
}
// GetChat
// @Title GetChat
// @Tag Chat API
// @Description get chat
// @Param id query string true "The id ( owner/name ) of the chat"
// @Success 200 {object} object.Chat The Response object
// @router /get-chat [get]
func (c *ApiController) GetChat() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedChat(object.GetChat(id))
c.ServeJSON()
}
// UpdateChat
// @Title UpdateChat
// @Tag Chat API
// @Description update chat
// @Param id query string true "The id ( owner/name ) of the chat"
// @Param body body object.Chat true "The details of the chat"
// @Success 200 {object} controllers.Response The Response object
// @router /update-chat [post]
func (c *ApiController) UpdateChat() {
id := c.Input().Get("id")
var chat object.Chat
err := json.Unmarshal(c.Ctx.Input.RequestBody, &chat)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateChat(id, &chat))
c.ServeJSON()
}
// AddChat
// @Title AddChat
// @Tag Chat API
// @Description add chat
// @Param body body object.Chat true "The details of the chat"
// @Success 200 {object} controllers.Response The Response object
// @router /add-chat [post]
func (c *ApiController) AddChat() {
var chat object.Chat
err := json.Unmarshal(c.Ctx.Input.RequestBody, &chat)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddChat(&chat))
c.ServeJSON()
}
// DeleteChat
// @Title DeleteChat
// @Tag Chat API
// @Description delete chat
// @Param body body object.Chat true "The details of the chat"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-chat [post]
func (c *ApiController) DeleteChat() {
var chat object.Chat
err := json.Unmarshal(c.Ctx.Input.RequestBody, &chat)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteChat(&chat))
c.ServeJSON()
}

View File

@ -65,7 +65,7 @@ func (c *ApiController) GetLdapUsers() {
// }) // })
//} //}
users, err := conn.GetLdapUsers(ldapServer.BaseDn) users, err := conn.GetLdapUsers(ldapServer)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -80,15 +80,16 @@ func (c *ApiController) GetLdapUsers() {
Cn: user.Cn, Cn: user.Cn,
GroupId: user.GidNumber, GroupId: user.GidNumber,
// GroupName: groupsMap[user.GidNumber].Cn, // GroupName: groupsMap[user.GidNumber].Cn,
Uuid: user.Uuid, Uuid: user.Uuid,
Email: util.GetMaxLenStr(user.Mail, user.Email, user.EmailAddress), DisplayName: user.DisplayName,
Phone: util.GetMaxLenStr(user.TelephoneNumber, user.Mobile, user.MobileTelephoneNumber), Email: util.GetMaxLenStr(user.Mail, user.Email, user.EmailAddress),
Address: util.GetMaxLenStr(user.RegisteredAddress, user.PostalAddress), Phone: util.GetMaxLenStr(user.TelephoneNumber, user.Mobile, user.MobileTelephoneNumber),
Address: util.GetMaxLenStr(user.RegisteredAddress, user.PostalAddress),
}) })
uuids = append(uuids, user.Uuid) uuids = append(uuids, user.Uuid)
} }
existUuids := object.CheckLdapUuidExist(ldapServer.Owner, uuids) existUuids := object.GetExistUuids(ldapServer.Owner, uuids)
c.ResponseOk(resp, existUuids) c.ResponseOk(resp, existUuids)
} }
@ -131,7 +132,7 @@ func (c *ApiController) AddLdap() {
return return
} }
if util.IsStringsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) { if util.IsStringsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Username, ldap.Password, ldap.BaseDn) {
c.ResponseError(c.T("general:Missing parameter")) c.ResponseError(c.T("general:Missing parameter"))
return return
} }
@ -160,7 +161,7 @@ func (c *ApiController) AddLdap() {
func (c *ApiController) UpdateLdap() { func (c *ApiController) UpdateLdap() {
var ldap object.Ldap var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap) err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil || util.IsStringsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) { if err != nil || util.IsStringsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Username, ldap.Password, ldap.BaseDn) {
c.ResponseError(c.T("general:Missing parameter")) c.ResponseError(c.T("general:Missing parameter"))
return return
} }

132
controllers/message.go Normal file
View File

@ -0,0 +1,132 @@
// 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/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetMessages
// @Title GetMessages
// @Tag Message API
// @Description get messages
// @Param owner query string true "The owner of messages"
// @Success 200 {array} object.Message The Response object
// @router /get-messages [get]
func (c *ApiController) GetMessages() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
chat := c.Input().Get("chat")
if limit == "" || page == "" {
var messages []*object.Message
if chat == "" {
messages = object.GetMessages(owner)
} else {
messages = object.GetChatMessages(chat)
}
c.Data["json"] = object.GetMaskedMessages(messages)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetMessageCount(owner, field, value)))
messages := object.GetMaskedMessages(object.GetPaginationMessages(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(messages, paginator.Nums())
}
}
// GetMessage
// @Title GetMessage
// @Tag Message API
// @Description get message
// @Param id query string true "The id ( owner/name ) of the message"
// @Success 200 {object} object.Message The Response object
// @router /get-message [get]
func (c *ApiController) GetMessage() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedMessage(object.GetMessage(id))
c.ServeJSON()
}
// UpdateMessage
// @Title UpdateMessage
// @Tag Message API
// @Description update message
// @Param id query string true "The id ( owner/name ) of the message"
// @Param body body object.Message true "The details of the message"
// @Success 200 {object} controllers.Response The Response object
// @router /update-message [post]
func (c *ApiController) UpdateMessage() {
id := c.Input().Get("id")
var message object.Message
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateMessage(id, &message))
c.ServeJSON()
}
// AddMessage
// @Title AddMessage
// @Tag Message API
// @Description add message
// @Param body body object.Message true "The details of the message"
// @Success 200 {object} controllers.Response The Response object
// @router /add-message [post]
func (c *ApiController) AddMessage() {
var message object.Message
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddMessage(&message))
c.ServeJSON()
}
// DeleteMessage
// @Title DeleteMessage
// @Tag Message API
// @Description delete message
// @Param body body object.Message true "The details of the message"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-message [post]
func (c *ApiController) DeleteMessage() {
var message object.Message
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteMessage(&message))
c.ServeJSON()
}

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

@ -124,40 +124,6 @@ func (c *ApiController) DeleteToken() {
c.ServeJSON() c.ServeJSON()
} }
// GetOAuthCode
// @Title GetOAuthCode
// @Tag Token API
// @Description get OAuth code
// @Param id query string true "The id ( owner/name ) of user"
// @Param client_id query string true "OAuth client id"
// @Param response_type query string true "OAuth response type"
// @Param redirect_uri query string true "OAuth redirect URI"
// @Param scope query string true "OAuth scope"
// @Param state query string true "OAuth state"
// @Success 200 {object} object.TokenWrapper The Response object
// @router /login/oauth/code [post]
func (c *ApiController) GetOAuthCode() {
userId := c.Input().Get("user_id")
clientId := c.Input().Get("client_id")
responseType := c.Input().Get("response_type")
redirectUri := c.Input().Get("redirect_uri")
scope := c.Input().Get("scope")
state := c.Input().Get("state")
nonce := c.Input().Get("nonce")
challengeMethod := c.Input().Get("code_challenge_method")
codeChallenge := c.Input().Get("code_challenge")
if challengeMethod != "S256" && challengeMethod != "null" && challengeMethod != "" {
c.ResponseError(c.T("auth:Challenge method should be S256"))
return
}
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, host, c.GetAcceptLanguage())
c.ServeJSON()
}
// GetOAuthToken // GetOAuthToken
// @Title GetOAuthToken // @Title GetOAuthToken
// @Tag Token API // @Tag Token API

View File

@ -95,13 +95,13 @@ func (c *ApiController) GetUser() {
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
if owner == "" { if owner == "" {
owner, _ = util.GetOwnerAndNameFromId(id) owner = util.GetOwnerFromId(id)
} }
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner)) organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner))
if !organization.IsProfilePublic { if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, owner, false, c.GetAcceptLanguage()) hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
if !hasPermission { if !hasPermission {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -138,10 +138,6 @@ func (c *ApiController) UpdateUser() {
id := c.Input().Get("id") id := c.Input().Get("id")
columnsStr := c.Input().Get("columns") columnsStr := c.Input().Get("columns")
if id == "" {
id = c.GetSessionUsername()
}
var user object.User var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user) err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil { if err != nil {
@ -149,24 +145,36 @@ func (c *ApiController) UpdateUser() {
return return
} }
if msg := object.CheckUpdateUser(object.GetUser(id), &user, c.GetAcceptLanguage()); msg != "" { if id == "" {
id = c.GetSessionUsername()
if id == "" {
c.ResponseError(c.T("general:Missing parameter"))
return
}
}
oldUser := object.GetUser(id)
if oldUser == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), id))
return
}
if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
return return
} }
isAdmin := c.IsAdmin()
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, c.GetAcceptLanguage()); !pass {
c.ResponseError(err)
return
}
columns := []string{} columns := []string{}
if columnsStr != "" { if columnsStr != "" {
columns = strings.Split(columnsStr, ",") columns = strings.Split(columnsStr, ",")
} }
isGlobalAdmin := c.IsGlobalAdmin() affected := object.UpdateUser(id, &user, columns, isAdmin)
if pass, err := checkPermissionForUpdateUser(id, user, c); !pass {
c.ResponseError(err)
return
}
affected := object.UpdateUser(id, &user, columns, isGlobalAdmin)
if affected { if affected {
object.UpdateUserToOriginalDatabase(&user) object.UpdateUserToOriginalDatabase(&user)
} }
@ -276,14 +284,34 @@ func (c *ApiController) SetPassword() {
userName := c.Ctx.Request.Form.Get("userName") userName := c.Ctx.Request.Form.Get("userName")
oldPassword := c.Ctx.Request.Form.Get("oldPassword") oldPassword := c.Ctx.Request.Form.Get("oldPassword")
newPassword := c.Ctx.Request.Form.Get("newPassword") newPassword := c.Ctx.Request.Form.Get("newPassword")
code := c.Ctx.Request.Form.Get("code")
if strings.Contains(newPassword, " ") {
c.ResponseError(c.T("user:New password cannot contain blank space."))
return
}
if len(newPassword) <= 5 {
c.ResponseError(c.T("user:New password must have at least 6 characters"))
return
}
requestUserId := c.GetSessionUsername()
userId := util.GetId(userOwner, userName) userId := util.GetId(userOwner, userName)
hasPermission, err := object.CheckUserPermission(requestUserId, userId, userOwner, true, c.GetAcceptLanguage()) requestUserId := c.GetSessionUsername()
if !hasPermission { if requestUserId == "" && code == "" {
c.ResponseError(err.Error())
return return
} else if code == "" {
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true, c.GetAcceptLanguage())
if !hasPermission {
c.ResponseError(err.Error())
return
}
} else {
if code != c.GetSession("verifiedCode") {
c.ResponseError("")
return
}
c.SetSession("verifiedCode", "")
} }
targetUser := object.GetUser(userId) targetUser := object.GetUser(userId)
@ -296,16 +324,6 @@ func (c *ApiController) SetPassword() {
} }
} }
if strings.Contains(newPassword, " ") {
c.ResponseError(c.T("user:New password cannot contain blank space."))
return
}
if len(newPassword) <= 5 {
c.ResponseError(c.T("user:New password must have at least 6 characters"))
return
}
targetUser.Password = newPassword targetUser.Password = newPassword
object.SetUserField(targetUser, "password", targetUser.Password) object.SetUserField(targetUser, "password", targetUser.Password)
c.ResponseOk() c.ResponseOk()

View File

@ -1,139 +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(userId string, newUser object.User, c *ApiController) (bool, string) {
oldUser := object.GetUser(userId)
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

@ -15,11 +15,13 @@
package controllers package controllers
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"github.com/casdoor/casdoor/captcha" "github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -31,60 +33,29 @@ const (
ForgetVerification = "forget" 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 ... // SendVerificationCode ...
// @Title SendVerificationCode // @Title SendVerificationCode
// @Tag Verification API // @Tag Verification API
// @router /send-verification-code [post] // @router /send-verification-code [post]
func (c *ApiController) SendVerificationCode() { func (c *ApiController) SendVerificationCode() {
destType := c.Ctx.Request.Form.Get("type") var vform form.VerificationForm
dest := c.Ctx.Request.Form.Get("dest") err := c.ParseForm(&vform)
countryCode := c.Ctx.Request.Form.Get("countryCode") if err != nil {
checkType := c.Ctx.Request.Form.Get("checkType") c.ResponseError(err.Error())
clientSecret := c.Ctx.Request.Form.Get("clientSecret") return
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")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request) remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if dest == "" { if msg := vform.CheckParameter(form.SendVerifyCode, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(c.T("general:Missing parameter") + ": dest.") c.ResponseError(msg)
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.")
return return
} }
if checkType != "none" { if vform.CaptchaType != "none" {
if captchaToken == "" { if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:Missing parameter") + ": captchaToken.") c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return return
} } else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
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 {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} else if !isHuman { } else if !isHuman {
@ -93,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)) organization := object.GetOrganization(util.GetId(application.Owner, application.Organization))
if organization == nil { if organization == nil {
c.ResponseError(c.T("check:Organization does not exist")) c.ResponseError(c.T("check:Organization does not exist"))
@ -102,57 +73,57 @@ func (c *ApiController) SendVerificationCode() {
var user *object.User var user *object.User
// checkUser != "", means method is ForgetVerification // checkUser != "", means method is ForgetVerification
if checkUser != "" { if vform.CheckUser != "" {
owner := application.Organization owner := application.Organization
user = object.GetUser(util.GetId(owner, checkUser)) user = object.GetUser(util.GetId(owner, vform.CheckUser))
} }
sendResp := errors.New("invalid dest type") sendResp := errors.New("invalid dest type")
switch destType { switch vform.Type {
case "email": case object.VerifyTypeEmail:
if !util.IsEmailValid(dest) { if !util.IsEmailValid(vform.Dest) {
c.ResponseError(c.T("check:Email is invalid")) c.ResponseError(c.T("check:Email is invalid"))
return return
} }
if method == LoginVerification || method == ForgetVerification { if vform.Method == LoginVerification || vform.Method == ForgetVerification {
if user != nil && util.GetMaskedEmail(user.Email) == dest { if user != nil && util.GetMaskedEmail(user.Email) == vform.Dest {
dest = user.Email vform.Dest = user.Email
} }
user = object.GetUserByEmail(organization.Name, dest) user = object.GetUserByEmail(organization.Name, vform.Dest)
if user == nil { if user == nil {
c.ResponseError(c.T("verification:the user does not exist, please sign up first")) c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return return
} }
} else if method == ResetVerification { } else if vform.Method == ResetVerification {
user = c.getCurrentUser() user = c.getCurrentUser()
} }
provider := application.GetEmailProvider() provider := application.GetEmailProvider()
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest) sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, vform.Dest)
case "phone": case object.VerifyTypePhone:
if method == LoginVerification || method == ForgetVerification { if vform.Method == LoginVerification || vform.Method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == dest { if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {
dest = user.Phone 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")) c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return return
} }
countryCode = user.GetCountryCode(countryCode) vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} else if method == ResetVerification { } else if vform.Method == ResetVerification {
if user = c.getCurrentUser(); user != nil { if user = c.getCurrentUser(); user != nil {
countryCode = user.GetCountryCode(countryCode) vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} }
} }
provider := application.GetSmsProvider() provider := application.GetSmsProvider()
if phone, ok := util.GetE164Number(dest, countryCode); !ok { 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"), countryCode)) c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))
return return
} else { } else {
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, phone) sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, phone)
@ -166,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 ... // ResetEmailOrPhone ...
// @Tag Account API // @Tag Account API
// @Title ResetEmailOrPhone // @Title ResetEmailOrPhone
@ -187,7 +190,7 @@ func (c *ApiController) ResetEmailOrPhone() {
checkDest := dest checkDest := dest
organization := object.GetOrganizationByUser(user) organization := object.GetOrganizationByUser(user)
if destType == "phone" { if destType == object.VerifyTypePhone {
if object.HasUserByField(user.Owner, "phone", dest) { if object.HasUserByField(user.Owner, "phone", dest) {
c.ResponseError(c.T("check:Phone already exists")) c.ResponseError(c.T("check:Phone already exists"))
return return
@ -199,7 +202,7 @@ func (c *ApiController) ResetEmailOrPhone() {
return 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) c.ResponseError(errMsg)
return return
} }
@ -207,7 +210,7 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), user.CountryCode)) c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), user.CountryCode))
return return
} }
} else if destType == "email" { } else if destType == object.VerifyTypeEmail {
if object.HasUserByField(user.Owner, "email", dest) { if object.HasUserByField(user.Owner, "email", dest) {
c.ResponseError(c.T("check:Email already exists")) c.ResponseError(c.T("check:Email already exists"))
return return
@ -219,21 +222,22 @@ func (c *ApiController) ResetEmailOrPhone() {
return 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) c.ResponseError(errMsg)
return return
} }
} }
if result := object.CheckVerificationCode(checkDest, code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess { if result := object.CheckVerificationCode(checkDest, code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg) c.ResponseError(result.Msg)
return return
} }
switch destType { switch destType {
case "email": case object.VerifyTypeEmail:
user.Email = dest user.Email = dest
object.SetUserField(user, "email", user.Email) object.SetUserField(user, "email", user.Email)
case "phone": case object.VerifyTypePhone:
user.Phone = dest user.Phone = dest
object.SetUserField(user, "phone", user.Phone) object.SetUserField(user, "phone", user.Phone)
default: default:
@ -245,35 +249,56 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseOk() c.ResponseOk()
} }
// VerifyCaptcha ... // VerifyCode
// @Title VerifyCaptcha
// @Tag Verification API // @Tag Verification API
// @router /verify-captcha [post] // @Title VerifyCode
func (c *ApiController) VerifyCaptcha() { // @router /api/verify-code [post]
captchaType := c.Ctx.Request.Form.Get("captchaType") func (c *ApiController) VerifyCode() {
var authForm form.AuthForm
captchaToken := c.Ctx.Request.Form.Get("captchaToken") err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
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 { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk(isValid) var user *object.User
if authForm.Name != "" {
user = object.GetUserByFields(authForm.Organization, authForm.Name)
}
var checkDest string
if strings.Contains(authForm.Username, "@") {
if user != nil && util.GetMaskedEmail(user.Email) == authForm.Username {
authForm.Username = user.Email
}
checkDest = authForm.Username
} else {
if user != nil && util.GetMaskedPhone(user.Phone) == authForm.Username {
authForm.Username = user.Phone
}
}
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(authForm.Username)
if verificationCodeType == object.VerifyTypePhone {
authForm.CountryCode = user.GetCountryCode(authForm.CountryCode)
var ok bool
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, authForm.Code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
}
object.DisableVerificationCode(checkDest)
c.SetSession("verifiedCode", authForm.Code)
c.ResponseOk()
} }

View File

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

6
go.mod
View File

@ -17,14 +17,16 @@ require (
github.com/casdoor/xorm-adapter/v3 v3.0.4 github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0 github.com/denisenkom/go-mssqldb v0.9.0
github.com/fogleman/gg v1.3.0
github.com/forestmgy/ldapserver v1.1.0 github.com/forestmgy/ldapserver v1.1.0
github.com/go-git/go-git/v5 v5.6.0 github.com/go-git/go-git/v5 v5.6.0
github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-mysql-org/go-mysql v1.7.0 github.com/go-mysql-org/go-mysql v1.7.0
github.com/go-pay/gopay v1.5.72 github.com/go-pay/gopay v1.5.72
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/go-webauthn/webauthn v0.8.2 github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
@ -34,6 +36,8 @@ require (
github.com/markbates/goth v1.75.2 github.com/markbates/goth v1.75.2
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5 github.com/nyaruka/phonenumbers v1.1.5
github.com/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/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0 github.com/russellhaering/gosaml2 v0.6.0

19
go.sum
View File

@ -173,6 +173,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/forestmgy/ldapserver v1.1.0 h1:gvil4nuLhqPEL8SugCkFhRyA0/lIvRdwZSqlrw63ll4= github.com/forestmgy/ldapserver v1.1.0 h1:gvil4nuLhqPEL8SugCkFhRyA0/lIvRdwZSqlrw63ll4=
github.com/forestmgy/ldapserver v1.1.0/go.mod h1:1RZ8lox1QSY7rmbjdmy+sYQXY4Lp7SpGzpdE3+j3IyM= github.com/forestmgy/ldapserver v1.1.0/go.mod h1:1RZ8lox1QSY7rmbjdmy+sYQXY4Lp7SpGzpdE3+j3IyM=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
@ -222,10 +224,10 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-webauthn/revoke v0.1.9 h1:gSJ1ckA9VaKA2GN4Ukp+kiGTk1/EXtaDb1YE8RknbS0= github.com/go-webauthn/revoke v0.1.6 h1:3tv+itza9WpX5tryRQx4GwxCCBrCIiJ8GIkOhxiAmmU=
github.com/go-webauthn/revoke v0.1.9/go.mod h1:j6WKPnv0HovtEs++paan9g3ar46gm1NarktkXBaPR+w= github.com/go-webauthn/revoke v0.1.6/go.mod h1:TB4wuW4tPlwgF3znujA96F70/YSQXHPPWl7vgY09Iy8=
github.com/go-webauthn/webauthn v0.8.2 h1:8KLIbpldjz9KVGHfqEgJNbkhd7bbRXhNw4QWFJE15oA= github.com/go-webauthn/webauthn v0.6.0 h1:uLInMApSvBfP+vEFasNE0rnVPG++fjp7lmAIvNhe+UU=
github.com/go-webauthn/webauthn v0.8.2/go.mod h1:d+ezx/jMCNDiqSMzOchuynKb9CVU1NM9BumOnokfcVQ= github.com/go-webauthn/webauthn v0.6.0/go.mod h1:7edMRZXwuM6JIVjN68G24Bzt+bPCvTmjiL0j+cAmXtY=
github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E= github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -234,10 +236,13 @@ github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptG
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -658,8 +663,10 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -674,6 +681,7 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -741,6 +749,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -833,6 +842,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -843,6 +853,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -32,8 +32,8 @@
"Email is invalid": "E-Mail ist ungültig", "Email is invalid": "E-Mail ist ungültig",
"Empty username.": "Leerer Benutzername.", "Empty username.": "Leerer Benutzername.",
"FirstName cannot be blank": "Vorname darf nicht leer sein", "FirstName cannot be blank": "Vorname darf nicht leer sein",
"LDAP user name or password incorrect": "Ldap Benutzername oder Passwort falsch",
"LastName cannot be blank": "Nachname darf nicht leer sein", "LastName cannot be blank": "Nachname darf nicht leer sein",
"Ldap user name or password incorrect": "Ldap Benutzername oder Passwort falsch",
"Multiple accounts with same uid, please check your ldap server": "Mehrere Konten mit derselben uid, bitte überprüfen Sie Ihren LDAP-Server", "Multiple accounts with same uid, please check your ldap server": "Mehrere Konten mit derselben uid, bitte überprüfen Sie Ihren LDAP-Server",
"Organization does not exist": "Organisation existiert nicht", "Organization does not exist": "Organisation existiert nicht",
"Password must have at least 6 characters": "Das Passwort muss mindestens 6 Zeichen enthalten", "Password must have at least 6 characters": "Das Passwort muss mindestens 6 Zeichen enthalten",
@ -42,6 +42,7 @@
"Phone number is invalid": "Die Telefonnummer ist ungültig", "Phone number is invalid": "Die Telefonnummer ist ungültig",
"Session outdated, please login again": "Sitzung abgelaufen, bitte erneut anmelden", "Session outdated, please login again": "Sitzung abgelaufen, bitte erneut anmelden",
"The user is forbidden to sign in, please contact the administrator": "Dem Benutzer ist der Zugang verboten, bitte kontaktieren Sie den Administrator", "The user is forbidden to sign in, please contact the administrator": "Dem Benutzer ist der Zugang verboten, bitte kontaktieren Sie den Administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Der Benutzername darf nur alphanumerische Zeichen, Unterstriche oder Bindestriche enthalten, keine aufeinanderfolgenden Bindestriche oder Unterstriche haben und darf nicht mit einem Bindestrich oder Unterstrich beginnen oder enden.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Der Benutzername darf nur alphanumerische Zeichen, Unterstriche oder Bindestriche enthalten, keine aufeinanderfolgenden Bindestriche oder Unterstriche haben und darf nicht mit einem Bindestrich oder Unterstrich beginnen oder enden.",
"Username already exists": "Benutzername existiert bereits", "Username already exists": "Benutzername existiert bereits",
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein", "Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein", "Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden", "Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Das Passwort oder der Code ist falsch. Du hast noch %d Versuche übrig", "password or code is incorrect, you have %d remaining chances": "Das Passwort oder der Code ist falsch. Du hast noch %d Versuche übrig",
"unsupported password type: %s": "Nicht unterstützter Passworttyp: %s" "unsupported password type: %s": "Nicht unterstützter Passworttyp: %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "Email is invalid", "Email is invalid": "Email is invalid",
"Empty username.": "Empty username.", "Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank", "FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank", "LastName cannot be blank": "LastName cannot be blank",
"Ldap user name or password incorrect": "Ldap user name or password incorrect",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server", "Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist", "Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters", "Password must have at least 6 characters": "Password must have at least 6 characters",
@ -42,6 +42,7 @@
"Phone number is invalid": "Phone number is invalid", "Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again", "Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator", "The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists", "Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address", "Username cannot be an email address": "Username cannot be an email address",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "Username must have at least 2 characters", "Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone", "Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances", "password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s" "unsupported password type: %s": "unsupported password type: %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "El correo electrónico no es válido", "Email is invalid": "El correo electrónico no es válido",
"Empty username.": "Nombre de usuario vacío.", "Empty username.": "Nombre de usuario vacío.",
"FirstName cannot be blank": "El nombre no puede estar en blanco", "FirstName cannot be blank": "El nombre no puede estar en blanco",
"LDAP user name or password incorrect": "Nombre de usuario o contraseña de Ldap incorrectos",
"LastName cannot be blank": "El apellido no puede estar en blanco", "LastName cannot be blank": "El apellido no puede estar en blanco",
"Ldap user name or password incorrect": "Nombre de usuario o contraseña de Ldap incorrectos",
"Multiple accounts with same uid, please check your ldap server": "Cuentas múltiples con el mismo uid, por favor revise su servidor ldap", "Multiple accounts with same uid, please check your ldap server": "Cuentas múltiples con el mismo uid, por favor revise su servidor ldap",
"Organization does not exist": "La organización no existe", "Organization does not exist": "La organización no existe",
"Password must have at least 6 characters": "La contraseña debe tener al menos 6 caracteres", "Password must have at least 6 characters": "La contraseña debe tener al menos 6 caracteres",
@ -42,6 +42,7 @@
"Phone number is invalid": "El número de teléfono no es válido", "Phone number is invalid": "El número de teléfono no es válido",
"Session outdated, please login again": "Sesión expirada, por favor vuelva a iniciar sesión", "Session outdated, please login again": "Sesión expirada, por favor vuelva a iniciar sesión",
"The user is forbidden to sign in, please contact the administrator": "El usuario no está autorizado a iniciar sesión, por favor contacte al administrador", "The user is forbidden to sign in, please contact the administrator": "El usuario no está autorizado a iniciar sesión, por favor contacte al administrador",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "El nombre de usuario solo puede contener caracteres alfanuméricos, guiones bajos o guiones, no puede tener guiones o subrayados consecutivos, y no puede comenzar ni terminar con un guión o subrayado.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "El nombre de usuario solo puede contener caracteres alfanuméricos, guiones bajos o guiones, no puede tener guiones o subrayados consecutivos, y no puede comenzar ni terminar con un guión o subrayado.",
"Username already exists": "El nombre de usuario ya existe", "Username already exists": "El nombre de usuario ya existe",
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico", "Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres", "Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono", "Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Contraseña o código incorrecto, tienes %d intentos restantes", "password or code is incorrect, you have %d remaining chances": "Contraseña o código incorrecto, tienes %d intentos restantes",
"unsupported password type: %s": "Tipo de contraseña no compatible: %s" "unsupported password type: %s": "Tipo de contraseña no compatible: %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "L'adresse e-mail est invalide", "Email is invalid": "L'adresse e-mail est invalide",
"Empty username.": "Nom d'utilisateur vide.", "Empty username.": "Nom d'utilisateur vide.",
"FirstName cannot be blank": "Le prénom ne peut pas être laissé vide", "FirstName cannot be blank": "Le prénom ne peut pas être laissé vide",
"LDAP user name or password incorrect": "Nom d'utilisateur ou mot de passe LDAP incorrect",
"LastName cannot be blank": "Le nom de famille ne peut pas être vide", "LastName cannot be blank": "Le nom de famille ne peut pas être vide",
"Ldap user name or password incorrect": "Nom d'utilisateur ou mot de passe LDAP incorrect",
"Multiple accounts with same uid, please check your ldap server": "Plusieurs comptes avec le même identifiant d'utilisateur, veuillez vérifier votre serveur LDAP", "Multiple accounts with same uid, please check your ldap server": "Plusieurs comptes avec le même identifiant d'utilisateur, veuillez vérifier votre serveur LDAP",
"Organization does not exist": "L'organisation n'existe pas", "Organization does not exist": "L'organisation n'existe pas",
"Password must have at least 6 characters": "Le mot de passe doit comporter au moins 6 caractères", "Password must have at least 6 characters": "Le mot de passe doit comporter au moins 6 caractères",
@ -42,6 +42,7 @@
"Phone number is invalid": "Le numéro de téléphone est invalide", "Phone number is invalid": "Le numéro de téléphone est invalide",
"Session outdated, please login again": "Session expirée, veuillez vous connecter à nouveau", "Session outdated, please login again": "Session expirée, veuillez vous connecter à nouveau",
"The user is forbidden to sign in, please contact the administrator": "L'utilisateur est interdit de se connecter, veuillez contacter l'administrateur", "The user is forbidden to sign in, please contact the administrator": "L'utilisateur est interdit de se connecter, veuillez contacter l'administrateur",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Le nom d'utilisateur ne peut contenir que des caractères alphanumériques, des traits soulignés ou des tirets, ne peut pas avoir de tirets ou de traits soulignés consécutifs et ne peut pas commencer ou se terminer par un tiret ou un trait souligné.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Le nom d'utilisateur ne peut contenir que des caractères alphanumériques, des traits soulignés ou des tirets, ne peut pas avoir de tirets ou de traits soulignés consécutifs et ne peut pas commencer ou se terminer par un tiret ou un trait souligné.",
"Username already exists": "Nom d'utilisateur existe déjà", "Username already exists": "Nom d'utilisateur existe déjà",
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail", "Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères", "Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone", "Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %d chances", "password or code is incorrect, you have %d remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %d chances",
"unsupported password type: %s": "Type de mot de passe non pris en charge : %s" "unsupported password type: %s": "Type de mot de passe non pris en charge : %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "Email tidak valid", "Email is invalid": "Email tidak valid",
"Empty username.": "Nama pengguna kosong.", "Empty username.": "Nama pengguna kosong.",
"FirstName cannot be blank": "Nama depan tidak boleh kosong", "FirstName cannot be blank": "Nama depan tidak boleh kosong",
"LDAP user name or password incorrect": "Nama pengguna atau kata sandi Ldap salah",
"LastName cannot be blank": "Nama belakang tidak boleh kosong", "LastName cannot be blank": "Nama belakang tidak boleh kosong",
"Ldap user name or password incorrect": "Nama pengguna atau kata sandi Ldap salah",
"Multiple accounts with same uid, please check your ldap server": "Beberapa akun dengan uid yang sama, harap periksa server ldap Anda", "Multiple accounts with same uid, please check your ldap server": "Beberapa akun dengan uid yang sama, harap periksa server ldap Anda",
"Organization does not exist": "Organisasi tidak ada", "Organization does not exist": "Organisasi tidak ada",
"Password must have at least 6 characters": "Kata sandi harus memiliki minimal 6 karakter", "Password must have at least 6 characters": "Kata sandi harus memiliki minimal 6 karakter",
@ -42,6 +42,7 @@
"Phone number is invalid": "Nomor telepon tidak valid", "Phone number is invalid": "Nomor telepon tidak valid",
"Session outdated, please login again": "Sesi kedaluwarsa, silakan masuk lagi", "Session outdated, please login again": "Sesi kedaluwarsa, silakan masuk lagi",
"The user is forbidden to sign in, please contact the administrator": "Pengguna dilarang masuk, silakan hubungi administrator", "The user is forbidden to sign in, please contact the administrator": "Pengguna dilarang masuk, silakan hubungi administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Nama pengguna hanya bisa menggunakan karakter alfanumerik, garis bawah atau tanda hubung, tidak boleh memiliki dua tanda hubung atau garis bawah berurutan, dan tidak boleh diawali atau diakhiri dengan tanda hubung atau garis bawah.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Nama pengguna hanya bisa menggunakan karakter alfanumerik, garis bawah atau tanda hubung, tidak boleh memiliki dua tanda hubung atau garis bawah berurutan, dan tidak boleh diawali atau diakhiri dengan tanda hubung atau garis bawah.",
"Username already exists": "Nama pengguna sudah ada", "Username already exists": "Nama pengguna sudah ada",
"Username cannot be an email address": "Username tidak bisa menjadi alamat email", "Username cannot be an email address": "Username tidak bisa menjadi alamat email",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter", "Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon", "Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Kata sandi atau kode salah, Anda memiliki %d kesempatan tersisa", "password or code is incorrect, you have %d remaining chances": "Kata sandi atau kode salah, Anda memiliki %d kesempatan tersisa",
"unsupported password type: %s": "jenis sandi tidak didukung: %s" "unsupported password type: %s": "jenis sandi tidak didukung: %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "電子メールは無効です", "Email is invalid": "電子メールは無効です",
"Empty username.": "空のユーザー名。", "Empty username.": "空のユーザー名。",
"FirstName cannot be blank": "ファーストネームは空白にできません", "FirstName cannot be blank": "ファーストネームは空白にできません",
"LDAP user name or password incorrect": "Ldapのユーザー名またはパスワードが間違っています",
"LastName cannot be blank": "姓は空白にできません", "LastName cannot be blank": "姓は空白にできません",
"Ldap user name or password incorrect": "Ldapのユーザー名またはパスワードが間違っています",
"Multiple accounts with same uid, please check your ldap server": "同じuidを持つ複数のアカウントがあります。あなたのLDAPサーバーを確認してください", "Multiple accounts with same uid, please check your ldap server": "同じuidを持つ複数のアカウントがあります。あなたのLDAPサーバーを確認してください",
"Organization does not exist": "組織は存在しません", "Organization does not exist": "組織は存在しません",
"Password must have at least 6 characters": "パスワードは少なくとも6つの文字が必要です", "Password must have at least 6 characters": "パスワードは少なくとも6つの文字が必要です",
@ -42,6 +42,7 @@
"Phone number is invalid": "電話番号が無効です", "Phone number is invalid": "電話番号が無効です",
"Session outdated, please login again": "セッションが期限切れになりました。再度ログインしてください", "Session outdated, please login again": "セッションが期限切れになりました。再度ログインしてください",
"The user is forbidden to sign in, please contact the administrator": "ユーザーはサインインできません。管理者に連絡してください", "The user is forbidden to sign in, please contact the administrator": "ユーザーはサインインできません。管理者に連絡してください",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "ユーザー名には英数字、アンダースコア、ハイフンしか含めることができません。連続したハイフンまたはアンダースコアは不可であり、ハイフンまたはアンダースコアで始まるまたは終わることもできません。", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "ユーザー名には英数字、アンダースコア、ハイフンしか含めることができません。連続したハイフンまたはアンダースコアは不可であり、ハイフンまたはアンダースコアで始まるまたは終わることもできません。",
"Username already exists": "ユーザー名はすでに存在しています", "Username already exists": "ユーザー名はすでに存在しています",
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません", "Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です", "Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません", "Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "パスワードまたはコードが間違っています。あと%d回の試行機会があります", "password or code is incorrect, you have %d remaining chances": "パスワードまたはコードが間違っています。あと%d回の試行機会があります",
"unsupported password type: %s": "サポートされていないパスワードタイプ:%s" "unsupported password type: %s": "サポートされていないパスワードタイプ:%s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "이메일이 유효하지 않습니다", "Email is invalid": "이메일이 유효하지 않습니다",
"Empty username.": "빈 사용자 이름.", "Empty username.": "빈 사용자 이름.",
"FirstName cannot be blank": "이름은 공백일 수 없습니다", "FirstName cannot be blank": "이름은 공백일 수 없습니다",
"LDAP user name or password incorrect": "LDAP 사용자 이름 또는 암호가 잘못되었습니다",
"LastName cannot be blank": "성은 비어 있을 수 없습니다", "LastName cannot be blank": "성은 비어 있을 수 없습니다",
"Ldap user name or password incorrect": "LDAP 사용자 이름 또는 암호가 잘못되었습니다",
"Multiple accounts with same uid, please check your ldap server": "동일한 UID를 가진 여러 계정이 있습니다. LDAP 서버를 확인해주세요", "Multiple accounts with same uid, please check your ldap server": "동일한 UID를 가진 여러 계정이 있습니다. LDAP 서버를 확인해주세요",
"Organization does not exist": "조직은 존재하지 않습니다", "Organization does not exist": "조직은 존재하지 않습니다",
"Password must have at least 6 characters": "암호는 적어도 6자 이상이어야 합니다", "Password must have at least 6 characters": "암호는 적어도 6자 이상이어야 합니다",
@ -42,6 +42,7 @@
"Phone number is invalid": "전화번호가 유효하지 않습니다", "Phone number is invalid": "전화번호가 유효하지 않습니다",
"Session outdated, please login again": "세션이 만료되었습니다. 다시 로그인해주세요", "Session outdated, please login again": "세션이 만료되었습니다. 다시 로그인해주세요",
"The user is forbidden to sign in, please contact the administrator": "사용자는 로그인이 금지되어 있습니다. 관리자에게 문의하십시오", "The user is forbidden to sign in, please contact the administrator": "사용자는 로그인이 금지되어 있습니다. 관리자에게 문의하십시오",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "사용자 이름은 알파벳, 숫자, 밑줄 또는 하이픈만 포함할 수 있으며, 연속된 하이픈 또는 밑줄을 가질 수 없으며, 하이픈 또는 밑줄로 시작하거나 끝날 수 없습니다.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "사용자 이름은 알파벳, 숫자, 밑줄 또는 하이픈만 포함할 수 있으며, 연속된 하이픈 또는 밑줄을 가질 수 없으며, 하이픈 또는 밑줄로 시작하거나 끝날 수 없습니다.",
"Username already exists": "사용자 이름이 이미 존재합니다", "Username already exists": "사용자 이름이 이미 존재합니다",
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다", "Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다", "Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다", "Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "암호 또는 코드가 올바르지 않습니다. %d번의 기회가 남아 있습니다", "password or code is incorrect, you have %d remaining chances": "암호 또는 코드가 올바르지 않습니다. %d번의 기회가 남아 있습니다",
"unsupported password type: %s": "지원되지 않는 암호 유형: %s" "unsupported password type: %s": "지원되지 않는 암호 유형: %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "Адрес электронной почты недействительный", "Email is invalid": "Адрес электронной почты недействительный",
"Empty username.": "Пустое имя пользователя.", "Empty username.": "Пустое имя пользователя.",
"FirstName cannot be blank": "Имя не может быть пустым", "FirstName cannot be blank": "Имя не может быть пустым",
"LDAP user name or password incorrect": "Неправильное имя пользователя или пароль Ldap",
"LastName cannot be blank": "Фамилия не может быть пустой", "LastName cannot be blank": "Фамилия не может быть пустой",
"Ldap user name or password incorrect": "Неправильное имя пользователя или пароль Ldap",
"Multiple accounts with same uid, please check your ldap server": "Множественные учетные записи с тем же UID. Пожалуйста, проверьте свой сервер LDAP", "Multiple accounts with same uid, please check your ldap server": "Множественные учетные записи с тем же UID. Пожалуйста, проверьте свой сервер LDAP",
"Organization does not exist": "Организация не существует", "Organization does not exist": "Организация не существует",
"Password must have at least 6 characters": "Пароль должен содержать не менее 6 символов", "Password must have at least 6 characters": "Пароль должен содержать не менее 6 символов",
@ -42,6 +42,7 @@
"Phone number is invalid": "Номер телефона является недействительным", "Phone number is invalid": "Номер телефона является недействительным",
"Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова", "Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова",
"The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору", "The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Имя пользователя может состоять только из буквенно-цифровых символов, нижних подчеркиваний или дефисов, не может содержать последовательные дефисы или подчеркивания, а также не может начинаться или заканчиваться на дефис или подчеркивание.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Имя пользователя может состоять только из буквенно-цифровых символов, нижних подчеркиваний или дефисов, не может содержать последовательные дефисы или подчеркивания, а также не может начинаться или заканчиваться на дефис или подчеркивание.",
"Username already exists": "Имя пользователя уже существует", "Username already exists": "Имя пользователя уже существует",
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты", "Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов", "Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону", "Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Неправильный пароль или код, у вас осталось %d попыток", "password or code is incorrect, you have %d remaining chances": "Неправильный пароль или код, у вас осталось %d попыток",
"unsupported password type: %s": "неподдерживаемый тип пароля: %s" "unsupported password type: %s": "неподдерживаемый тип пароля: %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "Địa chỉ email không hợp lệ", "Email is invalid": "Địa chỉ email không hợp lệ",
"Empty username.": "Tên đăng nhập trống.", "Empty username.": "Tên đăng nhập trống.",
"FirstName cannot be blank": "Tên không được để trống", "FirstName cannot be blank": "Tên không được để trống",
"LDAP user name or password incorrect": "Tên người dùng hoặc mật khẩu Ldap không chính xác",
"LastName cannot be blank": "Họ không thể để trống", "LastName cannot be blank": "Họ không thể để trống",
"Ldap user name or password incorrect": "Tên người dùng hoặc mật khẩu Ldap không chính xác",
"Multiple accounts with same uid, please check your ldap server": "Nhiều tài khoản với cùng một uid, vui lòng kiểm tra máy chủ ldap của bạn", "Multiple accounts with same uid, please check your ldap server": "Nhiều tài khoản với cùng một uid, vui lòng kiểm tra máy chủ ldap của bạn",
"Organization does not exist": "Tổ chức không tồn tại", "Organization does not exist": "Tổ chức không tồn tại",
"Password must have at least 6 characters": "Mật khẩu phải ít nhất 6 ký tự", "Password must have at least 6 characters": "Mật khẩu phải ít nhất 6 ký tự",
@ -42,6 +42,7 @@
"Phone number is invalid": "Số điện thoại không hợp lệ", "Phone number is invalid": "Số điện thoại không hợp lệ",
"Session outdated, please login again": "Phiên làm việc hết hạn, vui lòng đăng nhập lại", "Session outdated, please login again": "Phiên làm việc hết hạn, vui lòng đăng nhập lại",
"The user is forbidden to sign in, please contact the administrator": "Người dùng bị cấm đăng nhập, vui lòng liên hệ với quản trị viên", "The user is forbidden to sign in, please contact the administrator": "Người dùng bị cấm đăng nhập, vui lòng liên hệ với quản trị viên",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Tên người dùng chỉ có thể chứa các ký tự chữ và số, gạch dưới hoặc gạch ngang, không được có hai ký tự gạch dưới hoặc gạch ngang liền kề và không được bắt đầu hoặc kết thúc bằng dấu gạch dưới hoặc gạch ngang.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Tên người dùng chỉ có thể chứa các ký tự chữ và số, gạch dưới hoặc gạch ngang, không được có hai ký tự gạch dưới hoặc gạch ngang liền kề và không được bắt đầu hoặc kết thúc bằng dấu gạch dưới hoặc gạch ngang.",
"Username already exists": "Tên đăng nhập đã tồn tại", "Username already exists": "Tên đăng nhập đã tồn tại",
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email", "Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự", "Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại", "Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Mật khẩu hoặc mã không chính xác, bạn còn %d lần cơ hội", "password or code is incorrect, you have %d remaining chances": "Mật khẩu hoặc mã không chính xác, bạn còn %d lần cơ hội",
"unsupported password type: %s": "Loại mật khẩu không được hỗ trợ: %s" "unsupported password type: %s": "Loại mật khẩu không được hỗ trợ: %s"
}, },

View File

@ -32,8 +32,8 @@
"Email is invalid": "无效邮箱", "Email is invalid": "无效邮箱",
"Empty username.": "用户名不可为空", "Empty username.": "用户名不可为空",
"FirstName cannot be blank": "名不可以为空", "FirstName cannot be blank": "名不可以为空",
"LDAP user name or password incorrect": "LDAP密码错误",
"LastName cannot be blank": "姓不可以为空", "LastName cannot be blank": "姓不可以为空",
"Ldap user name or password incorrect": "LDAP密码错误",
"Multiple accounts with same uid, please check your ldap server": "多个帐户具有相同的uid请检查您的 LDAP 服务器", "Multiple accounts with same uid, please check your ldap server": "多个帐户具有相同的uid请检查您的 LDAP 服务器",
"Organization does not exist": "组织不存在", "Organization does not exist": "组织不存在",
"Password must have at least 6 characters": "新密码至少为6位", "Password must have at least 6 characters": "新密码至少为6位",
@ -42,6 +42,7 @@
"Phone number is invalid": "无效手机号", "Phone number is invalid": "无效手机号",
"Session outdated, please login again": "会话已过期,请重新登录", "Session outdated, please login again": "会话已过期,请重新登录",
"The user is forbidden to sign in, please contact the administrator": "该用户被禁止登录,请联系管理员", "The user is forbidden to sign in, please contact the administrator": "该用户被禁止登录,请联系管理员",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "用户名只能包含字母数字字符、下划线或连字符,不能有连续的连字符或下划线,也不能以连字符或下划线开头或结尾", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "用户名只能包含字母数字字符、下划线或连字符,不能有连续的连字符或下划线,也不能以连字符或下划线开头或结尾",
"Username already exists": "用户名已存在", "Username already exists": "用户名已存在",
"Username cannot be an email address": "用户名不可以是邮箱地址", "Username cannot be an email address": "用户名不可以是邮箱地址",
@ -51,6 +52,7 @@
"Username must have at least 2 characters": "用户名至少要有2个字符", "Username must have at least 2 characters": "用户名至少要有2个字符",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
"Your region is not allow to signup by phone": "所在地区不支持手机号注册", "Your region is not allow to signup by phone": "所在地区不支持手机号注册",
"password or code is incorrect": "密码错误",
"password or code is incorrect, you have %d remaining chances": "密码错误,您还有 %d 次尝试的机会", "password or code is incorrect, you have %d remaining chances": "密码错误,您还有 %d 次尝试的机会",
"unsupported password type: %s": "不支持的密码类型: %s" "unsupported password type: %s": "不支持的密码类型: %s"
}, },

View File

@ -159,8 +159,8 @@
"serverName": "", "serverName": "",
"host": "", "host": "",
"port": 389, "port": 389,
"admin": "", "username": "",
"passwd": "", "password": "",
"baseDn": "", "baseDn": "",
"autoSync": 0, "autoSync": 0,
"lastSync": "" "lastSync": ""

View File

@ -110,12 +110,11 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
for _, user := range users { for _, user := range users {
dn := fmt.Sprintf("cn=%s,%s", user.Name, string(r.BaseObject())) dn := fmt.Sprintf("cn=%s,%s", user.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn) e := ldap.NewSearchResultEntry(dn)
e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Name)) for _, attr := range r.Attributes() {
e.AddAttribute("email", message.AttributeValue(user.Email)) e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
e.AddAttribute("mobile", message.AttributeValue(user.Phone)) }
e.AddAttribute("userPassword", message.AttributeValue(getUserPasswordWithType(user)))
// e.AddAttribute("postalAddress", message.AttributeValue(user.Address[0]))
w.Write(e) w.Write(e)
} }
w.Write(res) w.Write(res)

View File

@ -21,6 +21,7 @@ import (
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/lor00x/goldap/message"
ldap "github.com/forestmgy/ldapserver" ldap "github.com/forestmgy/ldapserver"
) )
@ -68,6 +69,7 @@ func getUsername(filter string) string {
func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int) { func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int) {
r := m.GetSearchRequest() r := m.GetSearchRequest()
name, org, code := getNameAndOrgFromFilter(string(r.BaseObject()), r.FilterString()) name, org, code := getNameAndOrgFromFilter(string(r.BaseObject()), r.FilterString())
if code != ldap.LDAPResultSuccess { if code != ldap.LDAPResultSuccess {
return nil, code return nil, code
@ -85,7 +87,7 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
return nil, ldap.LDAPResultInsufficientAccessRights return nil, ldap.LDAPResultInsufficientAccessRights
} }
} else { } else {
hasPermission, err := object.CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), org, true, "en") hasPermission, err := object.CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), true, "en")
if !hasPermission { if !hasPermission {
log.Printf("ErrMsg = %v", err.Error()) log.Printf("ErrMsg = %v", err.Error())
return nil, ldap.LDAPResultInsufficientAccessRights return nil, ldap.LDAPResultInsufficientAccessRights
@ -114,3 +116,20 @@ func getUserPasswordWithType(user *object.User) string {
} }
return fmt.Sprintf("{%s}%s", prefix, user.Password) return fmt.Sprintf("{%s}%s", prefix, user.Password)
} }
func getAttribute(attributeName string, user *object.User) message.AttributeValue {
switch attributeName {
case "cn":
return message.AttributeValue(user.Name)
case "uid":
return message.AttributeValue(user.Name)
case "email":
return message.AttributeValue(user.Email)
case "mobile":
return message.AttributeValue(user.Phone)
case "userPassword":
return message.AttributeValue(getUserPasswordWithType(user))
default:
return ""
}
}

View File

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

View File

@ -201,6 +201,16 @@ func (a *Adapter) createTable() {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(Chat))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Message))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Product)) err = a.Engine.Sync2(new(Product))
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -80,3 +80,21 @@ func DownloadAndUpload(url string, fullFilePath string, lang string) {
panic(err) panic(err)
} }
} }
func getPermanentAvatarUrlFromBuffer(organization string, username string, fileBuffer *bytes.Buffer, ext string, upload bool) string {
if defaultStorageProvider == nil {
return ""
}
fullFilePath := fmt.Sprintf("/avatar/%s/%s%s", organization, username, ext)
uploadedFileUrl, _ := GetUploadFileUrl(defaultStorageProvider, fullFilePath, false)
if upload {
_, _, err := UploadFileSafe(defaultStorageProvider, fullFilePath, fileBuffer, "en")
if err != nil {
panic(err)
}
}
return uploadedFileUrl
}

View File

@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
@ -37,3 +38,22 @@ func TestSyncPermanentAvatars(t *testing.T) {
fmt.Printf("[%d/%d]: Update user: [%s]'s permanent avatar: %s\n", i, len(users), user.GetId(), user.PermanentAvatar) fmt.Printf("[%d/%d]: Update user: [%s]'s permanent avatar: %s\n", i, len(users), user.GetId(), user.PermanentAvatar)
} }
} }
func TestUpdateAvatars(t *testing.T) {
InitConfig()
InitDefaultStorageProvider()
proxy.InitHttpClient()
users := GetUsers("casdoor")
for _, user := range users {
if strings.HasPrefix(user.Avatar, "http") {
continue
}
updated := user.refreshAvatar()
if updated {
user.PermanentAvatar = "*"
UpdateUser(user.GetId(), user, []string{"avatar"}, true)
}
}
}

167
object/avatar_util.go Normal file
View File

@ -0,0 +1,167 @@
// 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 (
"bytes"
"crypto/md5"
"fmt"
"image"
"image/color"
"image/png"
"io"
"net/http"
"strings"
"github.com/fogleman/gg"
)
func hasGravatar(client *http.Client, email string) (bool, error) {
// Clean and lowercase the email
email = strings.TrimSpace(strings.ToLower(email))
// Generate MD5 hash of the email
hash := md5.New()
io.WriteString(hash, email)
hashedEmail := fmt.Sprintf("%x", hash.Sum(nil))
// Create Gravatar URL with d=404 parameter
gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=404", hashedEmail)
// Send a request to Gravatar
req, err := http.NewRequest("GET", gravatarURL, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check if the user has a custom Gravatar image
if resp.StatusCode == http.StatusOK {
return true, nil
} else if resp.StatusCode == http.StatusNotFound {
return false, nil
} else {
return false, fmt.Errorf("failed to fetch gravatar image: %s", resp.Status)
}
}
func getGravatarFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) {
// Clean and lowercase the email
email = strings.TrimSpace(strings.ToLower(email))
// Generate MD5 hash of the email
hash := md5.New()
io.WriteString(hash, email)
hashedEmail := fmt.Sprintf("%x", hash.Sum(nil))
// Create Gravatar URL
gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s", hashedEmail)
// Download the image
req, err := http.NewRequest("GET", gravatarURL, nil)
if err != nil {
return nil, "", err
}
resp, err := client.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("failed to download gravatar image: %s", resp.Status)
}
// Get the content type and determine the file extension
contentType := resp.Header.Get("Content-Type")
fileExtension := ""
switch contentType {
case "image/jpeg":
fileExtension = ".jpg"
case "image/png":
fileExtension = ".png"
case "image/gif":
fileExtension = ".gif"
default:
return nil, "", fmt.Errorf("unsupported content type: %s", contentType)
}
// Save the image to a bytes.Buffer
buffer := &bytes.Buffer{}
_, err = io.Copy(buffer, resp.Body)
if err != nil {
return nil, "", err
}
return buffer, fileExtension, nil
}
func getColor(data []byte) color.RGBA {
r := int(data[0]) % 256
g := int(data[1]) % 256
b := int(data[2]) % 256
return color.RGBA{uint8(r), uint8(g), uint8(b), 255}
}
func getIdenticonFileBuffer(username string) (*bytes.Buffer, string, error) {
username = strings.TrimSpace(strings.ToLower(username))
hash := md5.New()
io.WriteString(hash, username)
hashedUsername := hash.Sum(nil)
// Define the size of the image
const imageSize = 420
const cellSize = imageSize / 7
// Create a new image
img := image.NewRGBA(image.Rect(0, 0, imageSize, imageSize))
// Create a context
dc := gg.NewContextForRGBA(img)
// Set a background color
dc.SetColor(color.RGBA{240, 240, 240, 255})
dc.Clear()
// Get avatar color
avatarColor := getColor(hashedUsername)
// Draw cells
for i := 0; i < 7; i++ {
for j := 0; j < 7; j++ {
if (hashedUsername[i] >> uint(j) & 1) == 1 {
dc.SetColor(avatarColor)
dc.DrawRectangle(float64(j*cellSize), float64(i*cellSize), float64(cellSize), float64(cellSize))
dc.Fill()
}
}
}
// Save image to a bytes.Buffer
buffer := &bytes.Buffer{}
err := png.Encode(buffer, img)
if err != nil {
return nil, "", fmt.Errorf("failed to save image: %w", err)
}
return buffer, ".png", nil
}

147
object/chat.go Normal file
View File

@ -0,0 +1,147 @@
// 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"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type Chat struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Organization string `xorm:"varchar(100)" json:"organization"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Type string `xorm:"varchar(100)" json:"type"`
Category string `xorm:"varchar(100)" json:"category"`
User1 string `xorm:"varchar(100)" json:"user1"`
User2 string `xorm:"varchar(100)" json:"user2"`
Users []string `xorm:"varchar(100)" json:"users"`
MessageCount int `json:"messageCount"`
}
func GetMaskedChat(chat *Chat) *Chat {
if chat == nil {
return nil
}
return chat
}
func GetMaskedChats(chats []*Chat) []*Chat {
for _, chat := range chats {
chat = GetMaskedChat(chat)
}
return chats
}
func GetChatCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Chat{})
if err != nil {
panic(err)
}
return int(count)
}
func GetChats(owner string) []*Chat {
chats := []*Chat{}
err := adapter.Engine.Desc("created_time").Find(&chats, &Chat{Owner: owner})
if err != nil {
panic(err)
}
return chats
}
func GetPaginationChats(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Chat {
chats := []*Chat{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&chats)
if err != nil {
panic(err)
}
return chats
}
func getChat(owner string, name string) *Chat {
if owner == "" || name == "" {
return nil
}
chat := Chat{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&chat)
if err != nil {
panic(err)
}
if existed {
return &chat
} else {
return nil
}
}
func GetChat(id string) *Chat {
owner, name := util.GetOwnerAndNameFromId(id)
return getChat(owner, name)
}
func UpdateChat(id string, chat *Chat) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getChat(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(chat)
if err != nil {
panic(err)
}
return affected != 0
}
func AddChat(chat *Chat) bool {
affected, err := adapter.Engine.Insert(chat)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteChat(chat *Chat) bool {
affected, err := adapter.Engine.ID(core.PK{chat.Owner, chat.Name}).Delete(&Chat{})
if err != nil {
panic(err)
}
if affected != 0 {
return DeleteChatMessages(chat.Name)
}
return affected != 0
}
func (p *Chat) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

View File

@ -22,6 +22,7 @@ import (
"unicode" "unicode"
"github.com/casdoor/casdoor/cred" "github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
@ -42,86 +43,86 @@ func init() {
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`) 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 { if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist") return i18n.Translate(lang, "check:Organization does not exist")
} }
if application.IsSignupItemVisible("Username") { 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") 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") 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") 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") 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 return msg
} }
if HasUserByField(organization.Name, "name", username) { if HasUserByField(organization.Name, "name", form.Username) {
return i18n.Translate(lang, "check:Username already exists") 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") 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") 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") return i18n.Translate(lang, "check:Password must have at least 6 characters")
} }
if application.IsSignupItemVisible("Email") { if application.IsSignupItemVisible("Email") {
if email == "" { if form.Email == "" {
if application.IsSignupItemRequired("Email") { if application.IsSignupItemRequired("Email") {
return i18n.Translate(lang, "check:Email cannot be empty") return i18n.Translate(lang, "check:Email cannot be empty")
} }
} else { } else {
if HasUserByField(organization.Name, "email", email) { if HasUserByField(organization.Name, "email", form.Email) {
return i18n.Translate(lang, "check:Email already exists") 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") return i18n.Translate(lang, "check:Email is invalid")
} }
} }
} }
if application.IsSignupItemVisible("Phone") { if application.IsSignupItemVisible("Phone") {
if phone == "" { if form.Phone == "" {
if application.IsSignupItemRequired("Phone") { if application.IsSignupItemRequired("Phone") {
return i18n.Translate(lang, "check:Phone cannot be empty") return i18n.Translate(lang, "check:Phone cannot be empty")
} }
} else { } else {
if HasUserByField(organization.Name, "phone", phone) { if HasUserByField(organization.Name, "phone", form.Phone) {
return i18n.Translate(lang, "check:Phone already exists") 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") 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") return i18n.Translate(lang, "check:Phone number is invalid")
} }
} }
} }
if application.IsSignupItemVisible("Display name") { if application.IsSignupItemVisible("Display name") {
if application.GetSignupItemRule("Display name") == "First, last" && (firstName != "" || lastName != "") { if application.GetSignupItemRule("Display name") == "First, last" && (form.FirstName != "" || form.LastName != "") {
if firstName == "" { if form.FirstName == "" {
return i18n.Translate(lang, "check:FirstName cannot be blank") 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") return i18n.Translate(lang, "check:LastName cannot be blank")
} }
} else { } else {
if displayName == "" { if form.Name == "" {
return i18n.Translate(lang, "check:DisplayName cannot be blank") return i18n.Translate(lang, "check:DisplayName cannot be blank")
} else if application.GetSignupItemRule("Display name") == "Real name" { } 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") 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 application.IsSignupItemVisible("Affiliation") {
if affiliation == "" { if form.Affiliation == "" {
return i18n.Translate(lang, "check:Affiliation cannot be blank") return i18n.Translate(lang, "check:Affiliation cannot be blank")
} }
} }
@ -157,10 +158,16 @@ func checkSigninErrorTimes(user *User, lang string) string {
return "" return ""
} }
func CheckPassword(user *User, password string, lang string) string { func CheckPassword(user *User, password string, lang string, options ...bool) string {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// check the login error times // check the login error times
if msg := checkSigninErrorTimes(user, lang); msg != "" { if !enableCaptcha {
return msg if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
}
} }
organization := GetOrganizationByUser(user) organization := GetOrganizationByUser(user)
@ -182,35 +189,39 @@ func CheckPassword(user *User, password string, lang string) string {
return "" return ""
} }
return recordSigninErrorInfo(user, lang) return recordSigninErrorInfo(user, lang, enableCaptcha)
} else { } else {
return fmt.Sprintf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType) return fmt.Sprintf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
} }
} }
func checkLdapUserPassword(user *User, password string, lang string) (*User, string) { func checkLdapUserPassword(user *User, password string, lang string) string {
ldaps := GetLdaps(user.Owner) ldaps := GetLdaps(user.Owner)
ldapLoginSuccess := false ldapLoginSuccess := false
hit := false
for _, ldapServer := range ldaps { for _, ldapServer := range ldaps {
conn, err := ldapServer.GetLdapConn() conn, err := ldapServer.GetLdapConn()
if err != nil { if err != nil {
continue continue
} }
SearchFilter := fmt.Sprintf("(&(objectClass=posixAccount)(uid=%s))", user.Name)
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false, 0, 0, false, ldapServer.buildFilterString(user), []string{}, nil)
SearchFilter, []string{}, nil)
searchResult, err := conn.Conn.Search(searchReq) searchResult, err := conn.Conn.Search(searchReq)
if err != nil { if err != nil {
return nil, err.Error() return err.Error()
} }
if len(searchResult.Entries) == 0 { if len(searchResult.Entries) == 0 {
continue continue
} else if len(searchResult.Entries) > 1 { }
return nil, i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server") if len(searchResult.Entries) > 1 {
return i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server")
} }
hit = true
dn := searchResult.Entries[0].DN dn := searchResult.Entries[0].DN
if err := conn.Conn.Bind(dn, password); err == nil { if err := conn.Conn.Bind(dn, password); err == nil {
ldapLoginSuccess = true ldapLoginSuccess = true
@ -219,12 +230,19 @@ func checkLdapUserPassword(user *User, password string, lang string) (*User, str
} }
if !ldapLoginSuccess { if !ldapLoginSuccess {
return nil, i18n.Translate(lang, "check:Ldap user name or password incorrect") if !hit {
return "user not exist"
}
return i18n.Translate(lang, "check:LDAP user name or password incorrect")
} }
return user, "" return ""
} }
func CheckUserPassword(organization string, username string, password string, lang string) (*User, string) { func CheckUserPassword(organization string, username string, password string, lang string, options ...bool) (*User, string) {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
user := GetUserByFields(organization, username) user := GetUserByFields(organization, username)
if user == nil || user.IsDeleted == true { if user == nil || user.IsDeleted == true {
return nil, fmt.Sprintf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username)) return nil, fmt.Sprintf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username))
@ -236,10 +254,14 @@ func CheckUserPassword(organization string, username string, password string, la
if user.Ldap != "" { if user.Ldap != "" {
// ONLY for ldap users // ONLY for ldap users
return checkLdapUserPassword(user, password, lang) if msg := checkLdapUserPassword(user, password, lang); msg != "" {
if msg == "user not exist" {
return nil, fmt.Sprintf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
}
return nil, msg
}
} else { } else {
msg := CheckPassword(user, password, lang) if msg := CheckPassword(user, password, lang, enableCaptcha); msg != "" {
if msg != "" {
return nil, msg return nil, msg
} }
} }
@ -250,11 +272,13 @@ func filterField(field string) bool {
return reFieldWhiteList.MatchString(field) return reFieldWhiteList.MatchString(field)
} }
func CheckUserPermission(requestUserId, userId, userOwner string, strict bool, lang string) (bool, error) { func CheckUserPermission(requestUserId, userId string, strict bool, lang string) (bool, error) {
if requestUserId == "" { if requestUserId == "" {
return false, fmt.Errorf(i18n.Translate(lang, "general:Please login first")) return false, fmt.Errorf(i18n.Translate(lang, "general:Please login first"))
} }
userOwner := util.GetOwnerFromId(userId)
if userId != "" { if userId != "" {
targetUser := GetUser(userId) targetUser := GetUser(userId)
if targetUser == nil { if targetUser == nil {
@ -340,7 +364,7 @@ func CheckUsername(username string, lang string) string {
return "" return ""
} }
func CheckUpdateUser(oldUser *User, user *User, lang string) string { func CheckUpdateUser(oldUser, user *User, lang string) string {
if user.DisplayName == "" { if user.DisplayName == "" {
return i18n.Translate(lang, "user:Display name cannot be empty") return i18n.Translate(lang, "user:Display name cannot be empty")
} }
@ -367,7 +391,7 @@ func CheckUpdateUser(oldUser *User, user *User, lang string) string {
return "" return ""
} }
func CheckToEnableCaptcha(application *Application) bool { func CheckToEnableCaptcha(application *Application, organization, username string) bool {
if len(application.Providers) == 0 { if len(application.Providers) == 0 {
return false return false
} }
@ -377,6 +401,10 @@ func CheckToEnableCaptcha(application *Application) bool {
continue continue
} }
if providerItem.Provider.Category == "Captcha" { if providerItem.Provider.Category == "Captcha" {
if providerItem.Rule == "Dynamic" {
user := GetUserByFields(organization, username)
return user != nil && user.SigninWrongTimes >= SigninWrongTimesLimit
}
return providerItem.Rule == "Always" return providerItem.Rule == "Always"
} }
} }

View File

@ -45,9 +45,15 @@ func resetUserSigninErrorTimes(user *User) {
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, user.IsGlobalAdmin) UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, user.IsGlobalAdmin)
} }
func recordSigninErrorInfo(user *User, lang string) string { func recordSigninErrorInfo(user *User, lang string, options ...bool) string {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// increase failed login count // increase failed login count
user.SigninWrongTimes++ if user.SigninWrongTimes < SigninWrongTimesLimit {
user.SigninWrongTimes++
}
if user.SigninWrongTimes >= SigninWrongTimesLimit { if user.SigninWrongTimes >= SigninWrongTimesLimit {
// record the latest failed login time // record the latest failed login time
@ -57,10 +63,11 @@ func recordSigninErrorInfo(user *User, lang string) string {
// update user // update user
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, user.IsGlobalAdmin) UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, user.IsGlobalAdmin)
leftChances := SigninWrongTimesLimit - user.SigninWrongTimes leftChances := SigninWrongTimesLimit - user.SigninWrongTimes
if leftChances > 0 { if leftChances == 0 && enableCaptcha {
return fmt.Sprint(i18n.Translate(lang, "check:password or code is incorrect"))
} else if leftChances >= 0 {
return fmt.Sprintf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances) return fmt.Sprintf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances)
} }
// don't show the chance error message if the user has no chance left // don't show the chance error message if the user has no chance left
return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes())) return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes()))
} }

View File

@ -219,8 +219,8 @@ func initBuiltInLdap() {
ServerName: "BuildIn LDAP Server", ServerName: "BuildIn LDAP Server",
Host: "example.com", Host: "example.com",
Port: 389, Port: 389,
Admin: "cn=buildin,dc=example,dc=com", Username: "cn=buildin,dc=example,dc=com",
Passwd: "123", Password: "123",
BaseDn: "ou=BuildIn,dc=example,dc=com", BaseDn: "ou=BuildIn,dc=example,dc=com",
AutoSync: 0, AutoSync: 0,
LastSync: "", LastSync: "",

View File

@ -15,14 +15,7 @@
package object package object
import ( import (
"errors"
"fmt"
"strings"
"github.com/beego/beego"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr"
) )
type Ldap struct { type Ldap struct {
@ -30,263 +23,20 @@ type Ldap struct {
Owner string `xorm:"varchar(100)" json:"owner"` Owner string `xorm:"varchar(100)" json:"owner"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
ServerName string `xorm:"varchar(100)" json:"serverName"` ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `xorm:"int" json:"port"`
EnableSsl bool `xorm:"bool" json:"enableSsl"` EnableSsl bool `xorm:"bool" json:"enableSsl"`
Admin string `xorm:"varchar(100)" json:"admin"` Username string `xorm:"varchar(100)" json:"username"`
Passwd string `xorm:"varchar(100)" json:"passwd"` Password string `xorm:"varchar(100)" json:"password"`
BaseDn string `xorm:"varchar(100)" json:"baseDn"` BaseDn string `xorm:"varchar(100)" json:"baseDn"`
Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
AutoSync int `json:"autoSync"` AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"` LastSync string `xorm:"varchar(100)" json:"lastSync"`
} }
type ldapConn struct {
Conn *goldap.Conn
IsAD bool
}
//type ldapGroup struct {
// GidNumber string
// Cn string
//}
type ldapUser struct {
UidNumber string
Uid string
Cn string
GidNumber string
// Gcn string
Uuid string
Mail string
Email string
EmailAddress string
TelephoneNumber string
Mobile string
MobileTelephoneNumber string
RegisteredAddress string
PostalAddress string
}
type LdapRespUser struct {
UidNumber string `json:"uidNumber"`
Uid string `json:"uid"`
Cn string `json:"cn"`
GroupId string `json:"groupId"`
// GroupName string `json:"groupName"`
Uuid string `json:"uuid"`
Email string `json:"email"`
Phone string `json:"phone"`
Address string `json:"address"`
}
type ldapServerType struct {
Vendorname string
Vendorversion string
IsGlobalCatalogReady string
ForestFunctionality string
}
func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
returnAnyNotEmpty := func(strs ...string) string {
for _, str := range strs {
if str != "" {
return str
}
}
return ""
}
res := make([]LdapRespUser, 0)
for _, user := range users {
res = append(res, LdapRespUser{
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
Uuid: user.Uuid,
Email: returnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
Phone: returnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
Address: returnAnyNotEmpty(user.PostalAddress, user.RegisteredAddress),
})
}
return res
}
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
SearchFilter := "(objectClass=*)"
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
searchReq := goldap.NewSearchRequest("",
goldap.ScopeBaseObject, goldap.NeverDerefAliases, 0, 0, false,
SearchFilter, SearchAttributes, nil)
searchResult, err := Conn.Search(searchReq)
if err != nil {
return false, err
}
if len(searchResult.Entries) == 0 {
return false, nil
}
isMicrosoft := false
var ldapServerType ldapServerType
for _, entry := range searchResult.Entries {
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "vendorname":
ldapServerType.Vendorname = attribute.Values[0]
case "vendorversion":
ldapServerType.Vendorversion = attribute.Values[0]
case "isGlobalCatalogReady":
ldapServerType.IsGlobalCatalogReady = attribute.Values[0]
case "forestFunctionality":
ldapServerType.ForestFunctionality = attribute.Values[0]
}
}
}
if ldapServerType.Vendorname == "" &&
ldapServerType.Vendorversion == "" &&
ldapServerType.IsGlobalCatalogReady == "TRUE" &&
ldapServerType.ForestFunctionality != "" {
isMicrosoft = true
}
return isMicrosoft, err
}
func (ldap *Ldap) GetLdapConn() (c *ldapConn, err error) {
var conn *goldap.Conn
if ldap.EnableSsl {
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), nil)
} else {
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
}
if err != nil {
return nil, err
}
err = conn.Bind(ldap.Admin, ldap.Passwd)
if err != nil {
return nil, err
}
isAD, err := isMicrosoftAD(conn)
if err != nil {
return nil, err
}
return &ldapConn{Conn: conn, IsAD: isAD}, nil
}
//FIXME: The Base DN does not necessarily contain the Group
//func (l *ldapConn) GetLdapGroups(baseDn string) (map[string]ldapGroup, error) {
// SearchFilter := "(objectClass=posixGroup)"
// SearchAttributes := []string{"cn", "gidNumber"}
// groupMap := make(map[string]ldapGroup)
//
// searchReq := goldap.NewSearchRequest(baseDn,
// goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
// SearchFilter, SearchAttributes, nil)
// searchResult, err := l.Conn.Search(searchReq)
// if err != nil {
// return nil, err
// }
//
// if len(searchResult.Entries) == 0 {
// return nil, errors.New("no result")
// }
//
// for _, entry := range searchResult.Entries {
// var ldapGroupItem ldapGroup
// for _, attribute := range entry.Attributes {
// switch attribute.Name {
// case "gidNumber":
// ldapGroupItem.GidNumber = attribute.Values[0]
// break
// case "cn":
// ldapGroupItem.Cn = attribute.Values[0]
// break
// }
// }
// groupMap[ldapGroupItem.GidNumber] = ldapGroupItem
// }
//
// return groupMap, nil
//}
func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) {
SearchFilter := "(objectClass=posixAccount)"
SearchAttributes := []string{
"uidNumber", "uid", "cn", "gidNumber", "entryUUID", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
}
SearchFilterMsAD := "(objectClass=user)"
SearchAttributesMsAD := []string{
"uidNumber", "sAMAccountName", "cn", "gidNumber", "entryUUID", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
}
var searchReq *goldap.SearchRequest
if l.IsAD {
searchReq = goldap.NewSearchRequest(baseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
SearchFilterMsAD, SearchAttributesMsAD, nil)
} else {
searchReq = goldap.NewSearchRequest(baseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
SearchFilter, SearchAttributes, nil)
}
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
if err != nil {
return nil, err
}
if len(searchResult.Entries) == 0 {
return nil, errors.New("no result")
}
var ldapUsers []ldapUser
for _, entry := range searchResult.Entries {
var ldapUserItem ldapUser
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "uidNumber":
ldapUserItem.UidNumber = attribute.Values[0]
case "uid":
ldapUserItem.Uid = attribute.Values[0]
case "sAMAccountName":
ldapUserItem.Uid = attribute.Values[0]
case "cn":
ldapUserItem.Cn = attribute.Values[0]
case "gidNumber":
ldapUserItem.GidNumber = attribute.Values[0]
case "entryUUID":
ldapUserItem.Uuid = attribute.Values[0]
case "objectGUID":
ldapUserItem.Uuid = attribute.Values[0]
case "mail":
ldapUserItem.Mail = attribute.Values[0]
case "email":
ldapUserItem.Email = attribute.Values[0]
case "emailAddress":
ldapUserItem.EmailAddress = attribute.Values[0]
case "telephoneNumber":
ldapUserItem.TelephoneNumber = attribute.Values[0]
case "mobile":
ldapUserItem.Mobile = attribute.Values[0]
case "mobileTelephoneNumber":
ldapUserItem.MobileTelephoneNumber = attribute.Values[0]
case "registeredAddress":
ldapUserItem.RegisteredAddress = attribute.Values[0]
case "postalAddress":
ldapUserItem.PostalAddress = attribute.Values[0]
}
}
ldapUsers = append(ldapUsers, ldapUserItem)
}
return ldapUsers, nil
}
func AddLdap(ldap *Ldap) bool { func AddLdap(ldap *Ldap) bool {
if len(ldap.Id) == 0 { if len(ldap.Id) == 0 {
ldap.Id = util.GenerateId() ldap.Id = util.GenerateId()
@ -307,12 +57,12 @@ func AddLdap(ldap *Ldap) bool {
func CheckLdapExist(ldap *Ldap) bool { func CheckLdapExist(ldap *Ldap) bool {
var result []*Ldap var result []*Ldap
err := adapter.Engine.Find(&result, &Ldap{ err := adapter.Engine.Find(&result, &Ldap{
Owner: ldap.Owner, Owner: ldap.Owner,
Host: ldap.Host, Host: ldap.Host,
Port: ldap.Port, Port: ldap.Port,
Admin: ldap.Admin, Username: ldap.Username,
Passwd: ldap.Passwd, Password: ldap.Password,
BaseDn: ldap.BaseDn, BaseDn: ldap.BaseDn,
}) })
if err != nil { if err != nil {
panic(err) panic(err)
@ -359,7 +109,7 @@ func UpdateLdap(ldap *Ldap) bool {
} }
affected, err := adapter.Engine.ID(ldap.Id).Cols("owner", "server_name", "host", affected, err := adapter.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "enable_ssl", "admin", "passwd", "base_dn", "auto_sync").Update(ldap) "port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync").Update(ldap)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -375,123 +125,3 @@ func DeleteLdap(ldap *Ldap) bool {
return affected != 0 return affected != 0
} }
func SyncLdapUsers(owner string, users []LdapRespUser, ldapId string) (*[]LdapRespUser, *[]LdapRespUser) {
var existUsers []LdapRespUser
var failedUsers []LdapRespUser
var uuids []string
for _, user := range users {
uuids = append(uuids, user.Uuid)
}
existUuids := CheckLdapUuidExist(owner, uuids)
organization := getOrganization("admin", owner)
ldap := GetLdap(ldapId)
var dc []string
for _, basedn := range strings.Split(ldap.BaseDn, ",") {
if strings.Contains(basedn, "dc=") {
dc = append(dc, basedn[3:])
}
}
affiliation := strings.Join(dc, ".")
var ou []string
for _, admin := range strings.Split(ldap.Admin, ",") {
if strings.Contains(admin, "ou=") {
ou = append(ou, admin[3:])
}
}
tag := strings.Join(ou, ".")
for _, user := range users {
found := false
if len(existUuids) > 0 {
for _, existUuid := range existUuids {
if user.Uuid == existUuid {
existUsers = append(existUsers, user)
found = true
}
}
}
if !found && !AddUser(&User{
Owner: owner,
Name: buildLdapUserName(user.Uid, user.UidNumber),
CreatedTime: util.GetCurrentTime(),
DisplayName: user.Cn,
Avatar: organization.DefaultAvatar,
Email: user.Email,
Phone: user.Phone,
Address: []string{user.Address},
Affiliation: affiliation,
Tag: tag,
Score: beego.AppConfig.DefaultInt("initScore", 2000),
Ldap: user.Uuid,
}) {
failedUsers = append(failedUsers, user)
continue
}
}
return &existUsers, &failedUsers
}
func UpdateLdapSyncTime(ldapId string) {
_, err := adapter.Engine.ID(ldapId).Update(&Ldap{LastSync: util.GetCurrentTime()})
if err != nil {
panic(err)
}
}
func CheckLdapUuidExist(owner string, uuids []string) []string {
var results []User
var existUuids []string
existUuidSet := make(map[string]struct{})
//whereStr := ""
//for i, uuid := range uuids {
// if i == 0 {
// whereStr = fmt.Sprintf("'%s'", uuid)
// } else {
// whereStr = fmt.Sprintf(",'%s'", uuid)
// }
//}
err := adapter.Engine.Where(fmt.Sprintf("ldap IN (%s) AND owner = ?", "'"+strings.Join(uuids, "','")+"'"), owner).Find(&results)
if err != nil {
panic(err)
}
if len(results) > 0 {
for _, result := range results {
existUuidSet[result.Ldap] = struct{}{}
}
}
for uuid := range existUuidSet {
existUuids = append(existUuids, uuid)
}
return existUuids
}
func buildLdapUserName(uid, uidNum string) string {
var result User
uidWithNumber := fmt.Sprintf("%s_%s", uid, uidNum)
has, err := adapter.Engine.Where("name = ? or name = ?", uid, uidWithNumber).Get(&result)
if err != nil {
panic(err)
}
if has {
if result.Name == uid {
return uidWithNumber
}
return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6))
}
return uid
}

View File

@ -82,7 +82,7 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) {
continue continue
} }
users, err := conn.GetLdapUsers(ldap.BaseDn) users, err := conn.GetLdapUsers(ldap)
if err != nil { if err != nil {
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err)) logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
continue continue
@ -112,3 +112,10 @@ func (l *LdapAutoSynchronizer) LdapAutoSynchronizerStartUpAll() {
} }
} }
} }
func UpdateLdapSyncTime(ldapId string) {
_, err := adapter.Engine.ID(ldapId).Update(&Ldap{LastSync: util.GetCurrentTime()})
if err != nil {
panic(err)
}
}

403
object/ldap_conn.go Normal file
View File

@ -0,0 +1,403 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"errors"
"fmt"
"strings"
"github.com/beego/beego"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr"
)
type LdapConn struct {
Conn *goldap.Conn
IsAD bool
}
//type ldapGroup struct {
// GidNumber string
// Cn string
//}
type ldapUser struct {
UidNumber string
Uid string
Cn string
GidNumber string
// Gcn string
Uuid string
DisplayName string
Mail string
Email string
EmailAddress string
TelephoneNumber string
Mobile string
MobileTelephoneNumber string
RegisteredAddress string
PostalAddress string
}
type LdapRespUser struct {
UidNumber string `json:"uidNumber"`
Uid string `json:"uid"`
Cn string `json:"cn"`
GroupId string `json:"groupId"`
// GroupName string `json:"groupName"`
Uuid string `json:"uuid"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
Phone string `json:"phone"`
Address string `json:"address"`
}
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
var conn *goldap.Conn
if ldap.EnableSsl {
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), nil)
} else {
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
}
if err != nil {
return nil, err
}
err = conn.Bind(ldap.Username, ldap.Password)
if err != nil {
return nil, err
}
isAD, err := isMicrosoftAD(conn)
if err != nil {
return nil, err
}
return &LdapConn{Conn: conn, IsAD: isAD}, nil
}
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
SearchFilter := "(objectClass=*)"
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
searchReq := goldap.NewSearchRequest("",
goldap.ScopeBaseObject, goldap.NeverDerefAliases, 0, 0, false,
SearchFilter, SearchAttributes, nil)
searchResult, err := Conn.Search(searchReq)
if err != nil {
return false, err
}
if len(searchResult.Entries) == 0 {
return false, nil
}
isMicrosoft := false
type ldapServerType struct {
Vendorname string
Vendorversion string
IsGlobalCatalogReady string
ForestFunctionality string
}
var ldapServerTypes ldapServerType
for _, entry := range searchResult.Entries {
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "vendorname":
ldapServerTypes.Vendorname = attribute.Values[0]
case "vendorversion":
ldapServerTypes.Vendorversion = attribute.Values[0]
case "isGlobalCatalogReady":
ldapServerTypes.IsGlobalCatalogReady = attribute.Values[0]
case "forestFunctionality":
ldapServerTypes.ForestFunctionality = attribute.Values[0]
}
}
}
if ldapServerTypes.Vendorname == "" &&
ldapServerTypes.Vendorversion == "" &&
ldapServerTypes.IsGlobalCatalogReady == "TRUE" &&
ldapServerTypes.ForestFunctionality != "" {
isMicrosoft = true
}
return isMicrosoft, err
}
func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]ldapUser, error) {
SearchAttributes := []string{
"uidNumber", "cn", "sn", "gidNumber", "entryUUID", "displayName", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
}
if l.IsAD {
SearchAttributes = append(SearchAttributes, "sAMAccountName")
} else {
SearchAttributes = append(SearchAttributes, "uid")
}
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
0, 0, false,
ldapServer.Filter, SearchAttributes, nil)
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
if err != nil {
return nil, err
}
if len(searchResult.Entries) == 0 {
return nil, errors.New("no result")
}
var ldapUsers []ldapUser
for _, entry := range searchResult.Entries {
var user ldapUser
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "uidNumber":
user.UidNumber = attribute.Values[0]
case "uid":
user.Uid = attribute.Values[0]
case "sAMAccountName":
user.Uid = attribute.Values[0]
case "cn":
user.Cn = attribute.Values[0]
case "gidNumber":
user.GidNumber = attribute.Values[0]
case "entryUUID":
user.Uuid = attribute.Values[0]
case "objectGUID":
user.Uuid = attribute.Values[0]
case "displayName":
user.DisplayName = attribute.Values[0]
case "mail":
user.Mail = attribute.Values[0]
case "email":
user.Email = attribute.Values[0]
case "emailAddress":
user.EmailAddress = attribute.Values[0]
case "telephoneNumber":
user.TelephoneNumber = attribute.Values[0]
case "mobile":
user.Mobile = attribute.Values[0]
case "mobileTelephoneNumber":
user.MobileTelephoneNumber = attribute.Values[0]
case "registeredAddress":
user.RegisteredAddress = attribute.Values[0]
case "postalAddress":
user.PostalAddress = attribute.Values[0]
}
}
ldapUsers = append(ldapUsers, user)
}
return ldapUsers, nil
}
// FIXME: The Base DN does not necessarily contain the Group
//
// func (l *ldapConn) GetLdapGroups(baseDn string) (map[string]ldapGroup, error) {
// SearchFilter := "(objectClass=posixGroup)"
// SearchAttributes := []string{"cn", "gidNumber"}
// groupMap := make(map[string]ldapGroup)
//
// searchReq := goldap.NewSearchRequest(baseDn,
// goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
// SearchFilter, SearchAttributes, nil)
// searchResult, err := l.Conn.Search(searchReq)
// if err != nil {
// return nil, err
// }
//
// if len(searchResult.Entries) == 0 {
// return nil, errors.New("no result")
// }
//
// for _, entry := range searchResult.Entries {
// var ldapGroupItem ldapGroup
// for _, attribute := range entry.Attributes {
// switch attribute.Name {
// case "gidNumber":
// ldapGroupItem.GidNumber = attribute.Values[0]
// break
// case "cn":
// ldapGroupItem.Cn = attribute.Values[0]
// break
// }
// }
// groupMap[ldapGroupItem.GidNumber] = ldapGroupItem
// }
//
// return groupMap, nil
// }
func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
res := make([]LdapRespUser, 0)
for _, user := range users {
res = append(res, LdapRespUser{
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
Uuid: user.Uuid,
DisplayName: user.DisplayName,
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
Phone: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
Address: util.ReturnAnyNotEmpty(user.PostalAddress, user.RegisteredAddress),
})
}
return res
}
func SyncLdapUsers(owner string, respUsers []LdapRespUser, ldapId string) (*[]LdapRespUser, *[]LdapRespUser) {
var existUsers []LdapRespUser
var failedUsers []LdapRespUser
var uuids []string
for _, user := range respUsers {
uuids = append(uuids, user.Uuid)
}
existUuids := GetExistUuids(owner, uuids)
organization := getOrganization("admin", owner)
ldap := GetLdap(ldapId)
var dc []string
for _, basedn := range strings.Split(ldap.BaseDn, ",") {
if strings.Contains(basedn, "dc=") {
dc = append(dc, basedn[3:])
}
}
affiliation := strings.Join(dc, ".")
var ou []string
for _, admin := range strings.Split(ldap.Username, ",") {
if strings.Contains(admin, "ou=") {
ou = append(ou, admin[3:])
}
}
tag := strings.Join(ou, ".")
for _, respUser := range respUsers {
found := false
if len(existUuids) > 0 {
for _, existUuid := range existUuids {
if respUser.Uuid == existUuid {
existUsers = append(existUsers, respUser)
found = true
}
}
}
if !found {
newUser := &User{
Owner: owner,
Name: respUser.buildLdapUserName(),
CreatedTime: util.GetCurrentTime(),
DisplayName: respUser.buildLdapDisplayName(),
Avatar: organization.DefaultAvatar,
Email: respUser.Email,
Phone: respUser.Phone,
Address: []string{respUser.Address},
Affiliation: affiliation,
Tag: tag,
Score: beego.AppConfig.DefaultInt("initScore", 2000),
Ldap: respUser.Uuid,
}
affected := AddUser(newUser)
if !affected {
failedUsers = append(failedUsers, respUser)
continue
}
}
}
return &existUsers, &failedUsers
}
func GetExistUuids(owner string, uuids []string) []string {
var users []User
var existUuids []string
existUuidSet := make(map[string]struct{})
err := adapter.Engine.Where(fmt.Sprintf("ldap IN (%s) AND owner = ?", "'"+strings.Join(uuids, "','")+"'"), owner).Find(&users)
if err != nil {
panic(err)
}
if len(users) > 0 {
for _, result := range users {
existUuidSet[result.Ldap] = struct{}{}
}
}
for uuid := range existUuidSet {
existUuids = append(existUuids, uuid)
}
return existUuids
}
func (ldapUser *LdapRespUser) buildLdapUserName() string {
user := User{}
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
has, err := adapter.Engine.Where("name = ? or name = ?", ldapUser.Uid, uidWithNumber).Get(&user)
if err != nil {
panic(err)
}
if has {
if user.Name == ldapUser.Uid {
return uidWithNumber
}
return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6))
}
return ldapUser.Uid
}
func (ldapUser *LdapRespUser) buildLdapDisplayName() string {
if ldapUser.DisplayName != "" {
return ldapUser.DisplayName
}
return ldapUser.Cn
}
func (ldap *Ldap) buildFilterString(user *User) string {
if len(ldap.FilterFields) == 0 {
return fmt.Sprintf("(&%s(uid=%s))", ldap.Filter, user.Name)
}
filter := fmt.Sprintf("(&%s(|", ldap.Filter)
for _, field := range ldap.FilterFields {
filter = fmt.Sprintf("%s(%s=%s)", filter, field, user.getFieldFromLdapAttribute(field))
}
filter = fmt.Sprintf("%s))", filter)
return filter
}
func (user *User) getFieldFromLdapAttribute(attribute string) string {
switch attribute {
case "uid":
return user.Name
case "mail":
return user.Email
case "mobile":
return user.Phone
default:
return ""
}
}

157
object/message.go Normal file
View File

@ -0,0 +1,157 @@
// 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"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type Message struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Organization string `xorm:"varchar(100)" json:"organization"`
Chat string `xorm:"varchar(100) index" json:"chat"`
Author string `xorm:"varchar(100)" json:"author"`
Text string `xorm:"mediumtext" json:"text"`
}
func GetMaskedMessage(message *Message) *Message {
if message == nil {
return nil
}
return message
}
func GetMaskedMessages(messages []*Message) []*Message {
for _, message := range messages {
message = GetMaskedMessage(message)
}
return messages
}
func GetMessageCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Message{})
if err != nil {
panic(err)
}
return int(count)
}
func GetMessages(owner string) []*Message {
messages := []*Message{}
err := adapter.Engine.Desc("created_time").Find(&messages, &Message{Owner: owner})
if err != nil {
panic(err)
}
return messages
}
func GetChatMessages(chat string) []*Message {
messages := []*Message{}
err := adapter.Engine.Asc("created_time").Find(&messages, &Message{Chat: chat})
if err != nil {
panic(err)
}
return messages
}
func GetPaginationMessages(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Message {
messages := []*Message{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&messages)
if err != nil {
panic(err)
}
return messages
}
func getMessage(owner string, name string) *Message {
if owner == "" || name == "" {
return nil
}
message := Message{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&message)
if err != nil {
panic(err)
}
if existed {
return &message
} else {
return nil
}
}
func GetMessage(id string) *Message {
owner, name := util.GetOwnerAndNameFromId(id)
return getMessage(owner, name)
}
func UpdateMessage(id string, message *Message) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getMessage(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(message)
if err != nil {
panic(err)
}
return affected != 0
}
func AddMessage(message *Message) bool {
affected, err := adapter.Engine.Insert(message)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteMessage(message *Message) bool {
affected, err := adapter.Engine.ID(core.PK{message.Owner, message.Name}).Delete(&Message{})
if err != nil {
panic(err)
}
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

@ -18,6 +18,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"net"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -43,6 +44,26 @@ type OidcDiscovery struct {
EndSessionEndpoint string `json:"end_session_endpoint"` EndSessionEndpoint string `json:"end_session_endpoint"`
} }
func isIpAddress(host string) bool {
// Attempt to split the host and port, ignoring the error
hostWithoutPort, _, err := net.SplitHostPort(host)
if err != nil {
// If an error occurs, it might be because there's no port
// In that case, use the original host string
hostWithoutPort = host
}
// Attempt to parse the host as an IP address (both IPv4 and IPv6)
ip := net.ParseIP(hostWithoutPort)
if ip != nil {
// The host is an IP address
return true
}
// The host is not an IP address
return false
}
func getOriginFromHost(host string) (string, string) { func getOriginFromHost(host string) (string, string) {
origin := conf.GetConfigString("origin") origin := conf.GetConfigString("origin")
if origin != "" { if origin != "" {
@ -52,6 +73,8 @@ func getOriginFromHost(host string) (string, string) {
protocol := "https://" protocol := "https://"
if strings.HasPrefix(host, "localhost") { if strings.HasPrefix(host, "localhost") {
protocol = "http://" protocol = "http://"
} else if isIpAddress(host) {
protocol = "http://"
} }
if host == "localhost:8000" { if host == "localhost:8000" {

View File

@ -16,7 +16,6 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/casdoor/casdoor/cred" "github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
@ -210,14 +209,14 @@ func GetAccountItemByName(name string, organization *Organization) *AccountItem
return nil 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 { if accountItem == nil {
return true, "" return true, ""
} }
switch accountItem.ModifyRule { switch accountItem.ModifyRule {
case "Admin": 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) return false, fmt.Sprintf(i18n.Translate(lang, "organization:Only admin can modify the %s."), accountItem.Name)
} }
case "Immutable": case "Immutable":
@ -299,18 +298,16 @@ func organizationChangeTrigger(oldName string, newName string) error {
} }
for i, u := range role.Users { for i, u := range role.Users {
// u = organization/username // u = organization/username
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[0] == oldName { if name == oldName {
split[0] = newName role.Users[i] = util.GetId(owner, newName)
role.Users[i] = split[0] + "/" + split[1]
} }
} }
for i, u := range role.Roles { for i, u := range role.Roles {
// u = organization/username // u = organization/username
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[0] == oldName { if name == oldName {
split[0] = newName role.Roles[i] = util.GetId(owner, newName)
role.Roles[i] = split[0] + "/" + split[1]
} }
} }
role.Owner = newName role.Owner = newName
@ -326,18 +323,16 @@ func organizationChangeTrigger(oldName string, newName string) error {
} }
for i, u := range permission.Users { for i, u := range permission.Users {
// u = organization/username // u = organization/username
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[0] == oldName { if name == oldName {
split[0] = newName permission.Users[i] = util.GetId(owner, newName)
permission.Users[i] = split[0] + "/" + split[1]
} }
} }
for i, u := range permission.Roles { for i, u := range permission.Roles {
// u = organization/username // u = organization/username
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[0] == oldName { if name == oldName {
split[0] = newName permission.Roles[i] = util.GetId(owner, newName)
permission.Roles[i] = split[0] + "/" + split[1]
} }
} }
permission.Owner = newName permission.Owner = newName

View File

@ -29,7 +29,10 @@ import (
func getEnforcer(permission *Permission) *casbin.Enforcer { func getEnforcer(permission *Permission) *casbin.Enforcer {
tableName := "permission_rule" tableName := "permission_rule"
if len(permission.Adapter) != 0 { if len(permission.Adapter) != 0 {
tableName = permission.Adapter adapterObj := getCasbinAdapter(permission.Owner, permission.Adapter)
if adapterObj != nil && adapterObj.Table != "" {
tableName = adapterObj.Table
}
} }
tableNamePrefix := conf.GetConfigString("tableNamePrefix") tableNamePrefix := conf.GetConfigString("tableNamePrefix")
driverName := conf.GetConfigString("driverName") driverName := conf.GetConfigString("driverName")
@ -130,7 +133,7 @@ func getGroupingPolicies(permission *Permission) [][]string {
for _, subUser := range roleObj.Users { for _, subUser := range roleObj.Users {
if domainExist { if domainExist {
for _, domain := range permission.Domains { for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subUser, domain, role, "", "", permissionId}) groupingPolicies = append(groupingPolicies, []string{subUser, role, domain, "", "", permissionId})
} }
} else { } else {
groupingPolicies = append(groupingPolicies, []string{subUser, role, "", "", "", permissionId}) groupingPolicies = append(groupingPolicies, []string{subUser, role, "", "", "", permissionId})
@ -140,7 +143,7 @@ func getGroupingPolicies(permission *Permission) [][]string {
for _, subRole := range roleObj.Roles { for _, subRole := range roleObj.Roles {
if domainExist { if domainExist {
for _, domain := range permission.Domains { for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subRole, domain, role, "", "", permissionId}) groupingPolicies = append(groupingPolicies, []string{subRole, role, domain, "", "", permissionId})
} }
} else { } else {
groupingPolicies = append(groupingPolicies, []string{subRole, role, "", "", "", permissionId}) groupingPolicies = append(groupingPolicies, []string{subRole, role, "", "", "", permissionId})

View File

@ -30,7 +30,10 @@ func TestProduct(t *testing.T) {
product := GetProduct("admin/product_123") product := GetProduct("admin/product_123")
provider := getProvider(product.Owner, "provider_pay_alipay") provider := getProvider(product.Owner, "provider_pay_alipay")
cert := getCert(product.Owner, "cert-pay-alipay") cert := getCert(product.Owner, "cert-pay-alipay")
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey) pProvider, err := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, provider.ClientId2)
if err != nil {
panic(err)
}
paymentName := util.GenerateTimeId() paymentName := util.GenerateTimeId()
returnUrl := "" returnUrl := ""

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

@ -221,6 +221,10 @@ func UpdateProvider(id string, provider *Provider) bool {
if provider.ClientSecret2 == "***" { if provider.ClientSecret2 == "***" {
session = session.Omit("client_secret2") session = session.Omit("client_secret2")
} }
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
affected, err := session.Update(provider) affected, err := session.Update(provider)
if err != nil { if err != nil {
panic(err) panic(err)
@ -230,6 +234,9 @@ func UpdateProvider(id string, provider *Provider) bool {
} }
func AddProvider(provider *Provider) bool { func AddProvider(provider *Provider) bool {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
affected, err := adapter.Engine.Insert(provider) affected, err := adapter.Engine.Insert(provider)
if err != nil { if err != nil {
panic(err) panic(err)
@ -256,7 +263,11 @@ func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
} }
} }
pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey) pProvider, err := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, p.ClientId2)
if err != nil {
return nil, cert, err
}
if pProvider == nil { if pProvider == nil {
return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type) return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
} }

View File

@ -16,7 +16,6 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@ -182,13 +181,12 @@ func roleChangeTrigger(oldName string, newName string) error {
} }
for _, role := range roles { for _, role := range roles {
for j, u := range role.Roles { for j, u := range role.Roles {
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[1] == oldName { if name == oldName {
split[1] = newName role.Roles[j] = util.GetId(owner, newName)
role.Roles[j] = split[0] + "/" + split[1]
} }
} }
_, err = session.Where("name=?", role.Name).Update(role) _, err = session.Where("name=?", role.Name).And("owner=?", role.Owner).Update(role)
if err != nil { if err != nil {
return err return err
} }
@ -202,13 +200,12 @@ func roleChangeTrigger(oldName string, newName string) error {
for _, permission := range permissions { for _, permission := range permissions {
for j, u := range permission.Roles { for j, u := range permission.Roles {
// u = organization/username // u = organization/username
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[1] == oldName { if name == oldName {
split[1] = newName permission.Roles[j] = util.GetId(owner, newName)
permission.Roles[j] = split[0] + "/" + split[1]
} }
} }
_, err = session.Where("name=?", permission.Name).Update(permission) _, err = session.Where("name=?", permission.Name).And("owner=?", permission.Owner).Update(permission)
if err != nil { if err != nil {
return err return err
} }

View File

@ -23,29 +23,32 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
saml2 "github.com/russellhaering/gosaml2" saml2 "github.com/russellhaering/gosaml2"
dsig "github.com/russellhaering/goxmldsig" dsig "github.com/russellhaering/goxmldsig"
) )
func ParseSamlResponse(samlResponse string, providerType string) (string, error) { func ParseSamlResponse(samlResponse string, provider *Provider, host string) (string, error) {
samlResponse, _ = url.QueryUnescape(samlResponse) samlResponse, _ = url.QueryUnescape(samlResponse)
sp, err := buildSp(&Provider{Type: providerType}, samlResponse) sp, err := buildSp(provider, samlResponse, host)
if err != nil { if err != nil {
return "", err return "", err
} }
assertionInfo, err := sp.RetrieveAssertionInfo(samlResponse)
assertionInfo, err := sp.RetrieveAssertionInfo(samlResponse)
if err != nil {
return "", err
}
return assertionInfo.NameID, err return assertionInfo.NameID, err
} }
func GenerateSamlLoginUrl(id, relayState, lang string) (auth string, method string, err error) { func GenerateSamlRequest(id, relayState, host, lang string) (auth string, method string, err error) {
provider := GetProvider(id) provider := GetProvider(id)
if provider.Category != "SAML" { if provider.Category != "SAML" {
return "", "", fmt.Errorf(i18n.Translate(lang, "saml_sp:provider %s's category is not SAML"), provider.Name) return "", "", fmt.Errorf(i18n.Translate(lang, "saml_sp:provider %s's category is not SAML"), provider.Name)
} }
sp, err := buildSp(provider, "")
sp, err := buildSp(provider, "", host)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -67,35 +70,22 @@ func GenerateSamlLoginUrl(id, relayState, lang string) (auth string, method stri
return auth, method, nil return auth, method, nil
} }
func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvider, error) { func buildSp(provider *Provider, samlResponse string, host string) (*saml2.SAMLServiceProvider, error) {
origin := conf.GetConfigString("origin") _, origin := getOriginFromHost(host)
certStore := dsig.MemoryX509CertificateStore{ certStore, err := buildSpCertificateStore(provider, samlResponse)
Roots: []*x509.Certificate{},
}
certEncodedData := ""
if samlResponse != "" {
certEncodedData = parseSamlResponse(samlResponse, provider.Type)
} else if provider.IdP != "" {
certEncodedData = provider.IdP
}
certData, err := base64.StdEncoding.DecodeString(certEncodedData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
idpCert, err := x509.ParseCertificate(certData)
if err != nil {
return nil, err
}
certStore.Roots = append(certStore.Roots, idpCert)
sp := &saml2.SAMLServiceProvider{ sp := &saml2.SAMLServiceProvider{
ServiceProviderIssuer: fmt.Sprintf("%s/api/acs", origin), ServiceProviderIssuer: fmt.Sprintf("%s/api/acs", origin),
AssertionConsumerServiceURL: fmt.Sprintf("%s/api/acs", origin), AssertionConsumerServiceURL: fmt.Sprintf("%s/api/acs", origin),
IDPCertificateStore: &certStore,
SignAuthnRequests: false, SignAuthnRequests: false,
IDPCertificateStore: &certStore,
SPKeyStore: dsig.RandomKeyStoreForTest(), SPKeyStore: dsig.RandomKeyStoreForTest(),
} }
if provider.Endpoint != "" { if provider.Endpoint != "" {
sp.IdentityProviderSSOURL = provider.Endpoint sp.IdentityProviderSSOURL = provider.Endpoint
sp.IdentityProviderIssuer = provider.IssuerUrl sp.IdentityProviderIssuer = provider.IssuerUrl
@ -104,10 +94,45 @@ func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvide
sp.SignAuthnRequests = true sp.SignAuthnRequests = true
sp.SPKeyStore = buildSpKeyStore() sp.SPKeyStore = buildSpKeyStore()
} }
return sp, nil return sp, nil
} }
func parseSamlResponse(samlResponse string, providerType string) string { func buildSpKeyStore() dsig.X509KeyStore {
keyPair, err := tls.LoadX509KeyPair("object/token_jwt_key.pem", "object/token_jwt_key.key")
if err != nil {
panic(err)
}
return &dsig.TLSCertKeyStore{
PrivateKey: keyPair.PrivateKey,
Certificate: keyPair.Certificate,
}
}
func buildSpCertificateStore(provider *Provider, samlResponse string) (dsig.MemoryX509CertificateStore, error) {
certEncodedData := ""
if samlResponse != "" {
certEncodedData = getCertificateFromSamlResponse(samlResponse, provider.Type)
} else if provider.IdP != "" {
certEncodedData = provider.IdP
}
certData, err := base64.StdEncoding.DecodeString(certEncodedData)
if err != nil {
return dsig.MemoryX509CertificateStore{}, err
}
idpCert, err := x509.ParseCertificate(certData)
if err != nil {
return dsig.MemoryX509CertificateStore{}, err
}
certStore := dsig.MemoryX509CertificateStore{
Roots: []*x509.Certificate{idpCert},
}
return certStore, nil
}
func getCertificateFromSamlResponse(samlResponse string, providerType string) string {
de, err := base64.StdEncoding.DecodeString(samlResponse) de, err := base64.StdEncoding.DecodeString(samlResponse)
if err != nil { if err != nil {
panic(err) panic(err)
@ -122,14 +147,3 @@ func parseSamlResponse(samlResponse string, providerType string) string {
res := regexp.MustCompile(expression).FindStringSubmatch(deStr) res := regexp.MustCompile(expression).FindStringSubmatch(deStr)
return res[1] return res[1]
} }
func buildSpKeyStore() dsig.X509KeyStore {
keyPair, err := tls.LoadX509KeyPair("object/token_jwt_key.pem", "object/token_jwt_key.key")
if err != nil {
panic(err)
}
return &dsig.TLSCertKeyStore{
PrivateKey: keyPair.PrivateKey,
Certificate: keyPair.Certificate,
}
}

View File

@ -15,10 +15,12 @@
package object package object
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@ -48,6 +50,7 @@ type User struct {
EmailVerified bool `json:"emailVerified"` EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(20) index" json:"phone"` Phone string `xorm:"varchar(20) index" json:"phone"`
CountryCode string `xorm:"varchar(6)" json:"countryCode"` CountryCode string `xorm:"varchar(6)" json:"countryCode"`
Region string `xorm:"varchar(100)" json:"region"`
Location string `xorm:"varchar(100)" json:"location"` Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"` Address []string `json:"address"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"` Affiliation string `xorm:"varchar(100)" json:"affiliation"`
@ -57,7 +60,6 @@ type User struct {
Homepage string `xorm:"varchar(100)" json:"homepage"` Homepage string `xorm:"varchar(100)" json:"homepage"`
Bio string `xorm:"varchar(100)" json:"bio"` Bio string `xorm:"varchar(100)" json:"bio"`
Tag string `xorm:"varchar(100)" json:"tag"` Tag string `xorm:"varchar(100)" json:"tag"`
Region string `xorm:"varchar(100)" json:"region"`
Language string `xorm:"varchar(100)" json:"language"` Language string `xorm:"varchar(100)" json:"language"`
Gender string `xorm:"varchar(100)" json:"gender"` Gender string `xorm:"varchar(100)" json:"gender"`
Birthday string `xorm:"varchar(100)" json:"birthday"` Birthday string `xorm:"varchar(100)" json:"birthday"`
@ -423,7 +425,7 @@ func GetLastUser(owner string) *User {
return nil 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) owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
oldUser := getUser(owner, name) oldUser := getUser(owner, name)
if oldUser == nil { if oldUser == nil {
@ -449,12 +451,12 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
if len(columns) == 0 { if len(columns) == 0 {
columns = []string{ columns = []string{
"owner", "display_name", "avatar", "owner", "display_name", "avatar",
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application", "location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time", "signin_wrong_times", "last_signin_wrong_time",
} }
} }
if isGlobalAdmin { if isAdmin {
columns = append(columns, "name", "email", "phone", "country_code") columns = append(columns, "name", "email", "phone", "country_code")
} }
@ -513,7 +515,10 @@ func AddUser(user *User) bool {
user.UpdateUserHash() user.UpdateUserHash()
user.PreHash = user.Hash user.PreHash = user.Hash
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false) updated := user.refreshAvatar()
if updated && user.PermanentAvatar != "*" {
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
}
user.Ranking = GetUserCount(user.Owner, "", "") + 1 user.Ranking = GetUserCount(user.Owner, "", "") + 1
@ -652,13 +657,12 @@ func userChangeTrigger(oldName string, newName string) error {
for _, role := range roles { for _, role := range roles {
for j, u := range role.Users { for j, u := range role.Users {
// u = organization/username // u = organization/username
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[1] == oldName { if name == oldName {
split[1] = newName role.Users[j] = util.GetId(owner, newName)
role.Users[j] = split[0] + "/" + split[1]
} }
} }
_, err = session.Where("name=?", role.Name).Update(role) _, err = session.Where("name=?", role.Name).And("owner=?", role.Owner).Update(role)
if err != nil { if err != nil {
return err return err
} }
@ -672,13 +676,12 @@ func userChangeTrigger(oldName string, newName string) error {
for _, permission := range permissions { for _, permission := range permissions {
for j, u := range permission.Users { for j, u := range permission.Users {
// u = organization/username // u = organization/username
split := strings.Split(u, "/") owner, name := util.GetOwnerAndNameFromId(u)
if split[1] == oldName { if name == oldName {
split[1] = newName permission.Users[j] = util.GetId(owner, newName)
permission.Users[j] = split[0] + "/" + split[1]
} }
} }
_, err = session.Where("name=?", permission.Name).Update(permission) _, err = session.Where("name=?", permission.Name).And("owner=?", permission.Owner).Update(permission)
if err != nil { if err != nil {
return err return err
} }
@ -693,3 +696,40 @@ func userChangeTrigger(oldName string, newName string) error {
return session.Commit() return session.Commit()
} }
func (user *User) refreshAvatar() bool {
var err error
var fileBuffer *bytes.Buffer
var ext string
// Gravatar + Identicon
if strings.Contains(user.Avatar, "Gravatar") && user.Email != "" {
client := proxy.ProxyHttpClient
has, err := hasGravatar(client, user.Email)
if err != nil {
panic(err)
}
if has {
fileBuffer, ext, err = getGravatarFileBuffer(client, user.Email)
if err != nil {
panic(err)
}
}
}
if fileBuffer == nil && strings.Contains(user.Avatar, "Identicon") {
fileBuffer, ext, err = getIdenticonFileBuffer(user.Name)
if err != nil {
panic(err)
}
}
if fileBuffer != nil {
avatarUrl := getPermanentAvatarUrlFromBuffer(user.Owner, user.Name, fileBuffer, ext, true)
user.Avatar = avatarUrl
return true
}
return false
}

View File

@ -15,6 +15,7 @@
package object package object
import ( import (
"encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
@ -131,6 +132,12 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
if user.DisplayName == "" { if user.DisplayName == "" {
user.DisplayName = userInfo.DisplayName user.DisplayName = userInfo.DisplayName
} }
} else if user.DisplayName == "" {
if userInfo.Username != "" {
user.DisplayName = userInfo.Username
} else {
user.DisplayName = userInfo.Id
}
} }
if userInfo.Email != "" { if userInfo.Email != "" {
propertyName := fmt.Sprintf("oauth_%s_email", providerType) propertyName := fmt.Sprintf("oauth_%s_email", providerType)
@ -173,6 +180,116 @@ func ClearUserOAuthProperties(user *User, providerType string) bool {
return affected != 0 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 { func (user *User) GetCountryCode(countryCode string) string {
if countryCode != "" { if countryCode != "" {
return countryCode return countryCode
@ -187,3 +304,11 @@ func (user *User) GetCountryCode(countryCode string) string {
} }
return "" return ""
} }
func (user *User) IsAdminUser() bool {
if user == nil {
return false
}
return user.IsAdmin || user.IsGlobalAdmin
}

View File

@ -18,6 +18,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
"time" "time"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -38,6 +39,11 @@ const (
timeoutError = 3 timeoutError = 3
) )
const (
VerifyTypePhone = "phone"
VerifyTypeEmail = "email"
)
type VerificationRecord struct { type VerificationRecord struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
@ -213,6 +219,14 @@ func CheckSigninCode(user *User, dest, code, lang string) string {
} }
} }
func GetVerifyType(username string) (verificationCodeType string) {
if strings.Contains(username, "@") {
return VerifyTypeEmail
} else {
return VerifyTypeEmail
}
}
// From Casnode/object/validateCode.go line 116 // From Casnode/object/validateCode.go line 116
var stdNums = []byte("0123456789") var stdNums = []byte("0123456789")

View File

@ -28,21 +28,21 @@ type AlipayPaymentProvider struct {
Client *alipay.Client Client *alipay.Client
} }
func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider { func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) (*AlipayPaymentProvider, error) {
pp := &AlipayPaymentProvider{} pp := &AlipayPaymentProvider{}
client, err := alipay.NewClient(appId, appPrivateKey, true) client, err := alipay.NewClient(appId, appPrivateKey, true)
if err != nil { if err != nil {
panic(err) return nil, err
} }
err = client.SetCertSnByContent([]byte(appCertificate), []byte(authorityRootPublicKey), []byte(authorityPublicKey)) err = client.SetCertSnByContent([]byte(appCertificate), []byte(authorityRootPublicKey), []byte(authorityPublicKey))
if err != nil { if err != nil {
panic(err) return nil, err
} }
pp.Client = client pp.Client = client
return pp return pp, nil
} }
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) { func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {

View File

@ -22,11 +22,23 @@ type PaymentProvider interface {
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
} }
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider { func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string, clientId2 string) (PaymentProvider, error) {
if typ == "Alipay" { if typ == "Alipay" {
return NewAlipayPaymentProvider(appId, appCertificate, appPrivateKey, authorityPublicKey, authorityRootPublicKey) newAlipayPaymentProvider, err := NewAlipayPaymentProvider(appId, appCertificate, appPrivateKey, authorityPublicKey, authorityRootPublicKey)
if err != nil {
return nil, err
}
return newAlipayPaymentProvider, nil
} else if typ == "GC" { } else if typ == "GC" {
return NewGcPaymentProvider(appId, clientSecret, host) return NewGcPaymentProvider(appId, clientSecret, host), nil
} else if typ == "WeChat Pay" {
// appId, mchId, mchCertSerialNumber, apiV3Key, privateKey
newWechatPaymentProvider, err := NewWechatPaymentProvider(clientId2, appId, appCertificate, clientSecret, appPrivateKey)
if err != nil {
return nil, err
}
return newWechatPaymentProvider, nil
} }
return nil
return nil, nil
} }

102
pp/wechatpay.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2022 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 pp
import (
"context"
"fmt"
"net/http"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
)
type WechatPaymentProvider struct {
ClientV3 *wechat.ClientV3
appId string
}
func NewWechatPaymentProvider(appId string, mchId string, mchCertSerialNumber string, apiV3Key string, privateKey string) (*WechatPaymentProvider, error) {
pp := &WechatPaymentProvider{appId: appId}
clientV3, err := wechat.NewClientV3(mchId, mchCertSerialNumber, apiV3Key, privateKey)
if err != nil {
return nil, err
}
err = clientV3.AutoVerifySign()
if err != nil {
return nil, err
}
pp.ClientV3 = clientV3
return pp, nil
}
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
bm.Set("providerName", providerName)
bm.Set("productName", productName)
bm.Set("return_url", returnUrl)
bm.Set("notify_url", notifyUrl)
bm.Set("body", productDisplayName)
bm.Set("out_trade_no", paymentName)
bm.Set("total_fee", getPriceString(price))
wechatRsp, err := pp.ClientV3.V3TransactionJsapi(context.Background(), bm)
if err != nil {
return "", err
}
payUrl := fmt.Sprintf("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect", pp.appId, wechatRsp.Response.PrepayId)
return payUrl, nil
}
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
bm, err := wechat.V3ParseNotifyToBodyMap(request)
if err != nil {
return "", "", 0, "", "", err
}
providerName := bm.Get("providerName")
productName := bm.Get("productName")
productDisplayName := bm.Get("body")
paymentName := bm.Get("out_trade_no")
price := util.ParseFloat(bm.Get("total_fee"))
notifyReq, err := wechat.V3ParseNotify(request)
if err != nil {
panic(err)
}
cert := pp.ClientV3.WxPublicKey()
err = notifyReq.VerifySignByPK(cert)
if err != nil {
return "", "", 0, "", "", err
}
return productDisplayName, paymentName, price, productName, providerName, nil
}
func (pp *WechatPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
return "", nil
}

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 ( import (
"github.com/beego/beego" "github.com/beego/beego"
"github.com/casdoor/casdoor/controllers" "github.com/casdoor/casdoor/controllers"
"github.com/prometheus/client_golang/prometheus/promhttp"
) )
func init() { func init() {
@ -57,6 +57,7 @@ func initAPI() {
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta") beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent") beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType") beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations") beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization") beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
@ -115,6 +116,7 @@ func initAPI() {
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone") beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone")
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode") beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/verify-code", &controllers.ApiController{}, "POST:VerifyCode")
beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha") beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone") beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha") beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha")
@ -154,7 +156,6 @@ func initAPI() {
beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken") beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken")
beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken") beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken")
beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken") beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken")
beego.Router("/api/login/oauth/code", &controllers.ApiController{}, "POST:GetOAuthCode")
beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken") beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken") beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
beego.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken") beego.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken")
@ -188,6 +189,18 @@ func initAPI() {
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert") beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert") beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/get-chats", &controllers.ApiController{}, "GET:GetChats")
beego.Router("/api/get-chat", &controllers.ApiController{}, "GET:GetChat")
beego.Router("/api/update-chat", &controllers.ApiController{}, "POST:UpdateChat")
beego.Router("/api/add-chat", &controllers.ApiController{}, "POST:AddChat")
beego.Router("/api/delete-chat", &controllers.ApiController{}, "POST:DeleteChat")
beego.Router("/api/get-messages", &controllers.ApiController{}, "GET:GetMessages")
beego.Router("/api/get-message", &controllers.ApiController{}, "GET:GetMessage")
beego.Router("/api/update-message", &controllers.ApiController{}, "POST:UpdateMessage")
beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage")
beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage")
beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts") beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts")
beego.Router("/api/get-product", &controllers.ApiController{}, "GET:GetProduct") beego.Router("/api/get-product", &controllers.ApiController{}, "GET:GetProduct")
beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct") beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")
@ -226,4 +239,7 @@ func initAPI() {
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo") beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo") 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

@ -51,6 +51,12 @@ func StaticFilter(ctx *context.Context) {
path += urlPath path += urlPath
} }
path2 := strings.TrimLeft(path, "web/build/images/")
if util.FileExist(path2) {
http.ServeFile(ctx.ResponseWriter, ctx.Request, path2)
return
}
if !util.FileExist(path) { if !util.FileExist(path) {
path = "web/build/index.html" path = "web/build/index.html"
} }

View File

@ -99,6 +99,34 @@
} }
} }
}, },
"/api/add-chat": {
"post": {
"tags": [
"Chat API"
],
"description": "add chat",
"operationId": "ApiController.AddChat",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the chat",
"required": true,
"schema": {
"$ref": "#/definitions/object.Chat"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/add-ldap": { "/api/add-ldap": {
"post": { "post": {
"tags": [ "tags": [
@ -107,6 +135,34 @@
"operationId": "ApiController.AddLdap" "operationId": "ApiController.AddLdap"
} }
}, },
"/api/add-message": {
"post": {
"tags": [
"Message API"
],
"description": "add message",
"operationId": "ApiController.AddMessage",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the message",
"required": true,
"schema": {
"$ref": "#/definitions/object.Message"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/add-model": { "/api/add-model": {
"post": { "post": {
"tags": [ "tags": [
@ -495,6 +551,32 @@
"operationId": "ApiController.GetCaptcha" "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": { "/api/api/get-webhook-event": {
"get": { "get": {
"tags": [ "tags": [
@ -595,6 +677,14 @@
} }
} }
}, },
"/api/api/verify-code": {
"post": {
"tags": [
"Verification API"
],
"operationId": "ApiController.VerifyCode"
}
},
"/api/api/webhook": { "/api/api/webhook": {
"post": { "post": {
"tags": [ "tags": [
@ -700,6 +790,34 @@
} }
} }
}, },
"/api/delete-chat": {
"post": {
"tags": [
"Chat API"
],
"description": "delete chat",
"operationId": "ApiController.DeleteChat",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the chat",
"required": true,
"schema": {
"$ref": "#/definitions/object.Chat"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/delete-ldap": { "/api/delete-ldap": {
"post": { "post": {
"tags": [ "tags": [
@ -708,6 +826,34 @@
"operationId": "ApiController.DeleteLdap" "operationId": "ApiController.DeleteLdap"
} }
}, },
"/api/delete-message": {
"post": {
"tags": [
"Message API"
],
"description": "delete message",
"operationId": "ApiController.DeleteMessage",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the message",
"required": true,
"schema": {
"$ref": "#/definitions/object.Message"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/delete-model": { "/api/delete-model": {
"post": { "post": {
"tags": [ "tags": [
@ -1234,6 +1380,61 @@
} }
} }
}, },
"/api/get-chat": {
"get": {
"tags": [
"Chat API"
],
"description": "get chat",
"operationId": "ApiController.GetChat",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of the chat",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.Chat"
}
}
}
}
},
"/api/get-chats": {
"get": {
"tags": [
"Chat API"
],
"description": "get chats",
"operationId": "ApiController.GetChats",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "The owner of chats",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Chat"
}
}
}
}
}
},
"/api/get-default-application": { "/api/get-default-application": {
"get": { "get": {
"tags": [ "tags": [
@ -1357,6 +1558,61 @@
"operationId": "ApiController.GetLdaps" "operationId": "ApiController.GetLdaps"
} }
}, },
"/api/get-message": {
"get": {
"tags": [
"Message API"
],
"description": "get message",
"operationId": "ApiController.GetMessage",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of the message",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.Message"
}
}
}
}
},
"/api/get-messages": {
"get": {
"tags": [
"Message API"
],
"description": "get messages",
"operationId": "ApiController.GetMessages",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "The owner of messages",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Message"
}
}
}
}
}
},
"/api/get-model": { "/api/get-model": {
"get": { "get": {
"tags": [ "tags": [
@ -2514,7 +2770,7 @@
"description": "Login information", "description": "Login information",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/controllers.RequestForm" "$ref": "#/definitions/controllers.AuthForm"
} }
} }
], ],
@ -2587,67 +2843,6 @@
} }
} }
}, },
"/api/login/oauth/code": {
"post": {
"tags": [
"Token API"
],
"description": "get OAuth code",
"operationId": "ApiController.GetOAuthCode",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of user",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "client_id",
"description": "OAuth client id",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "response_type",
"description": "OAuth response type",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "redirect_uri",
"description": "OAuth redirect URI",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "scope",
"description": "OAuth scope",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "state",
"description": "OAuth state",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.TokenWrapper"
}
}
}
}
},
"/api/login/oauth/introspect": { "/api/login/oauth/introspect": {
"post": { "post": {
"description": "The introspection endpoint is an OAuth 2.0 endpoint that takes a", "description": "The introspection endpoint is an OAuth 2.0 endpoint that takes a",
@ -3056,6 +3251,41 @@
} }
} }
}, },
"/api/update-chat": {
"post": {
"tags": [
"Chat API"
],
"description": "update chat",
"operationId": "ApiController.UpdateChat",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of the chat",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"description": "The details of the chat",
"required": true,
"schema": {
"$ref": "#/definitions/object.Chat"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/update-ldap": { "/api/update-ldap": {
"post": { "post": {
"tags": [ "tags": [
@ -3064,6 +3294,41 @@
"operationId": "ApiController.UpdateLdap" "operationId": "ApiController.UpdateLdap"
} }
}, },
"/api/update-message": {
"post": {
"tags": [
"Message API"
],
"description": "update message",
"operationId": "ApiController.UpdateMessage",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of the message",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"description": "The details of the message",
"required": true,
"schema": {
"$ref": "#/definitions/object.Message"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/update-model": { "/api/update-model": {
"post": { "post": {
"tags": [ "tags": [
@ -3644,11 +3909,11 @@
} }
}, },
"definitions": { "definitions": {
"2306.0xc0003a4480.false": { "1183.0xc000455050.false": {
"title": "false", "title": "false",
"type": "object" "type": "object"
}, },
"2340.0xc0003a44b0.false": { "1217.0xc000455080.false": {
"title": "false", "title": "false",
"type": "object" "type": "object"
}, },
@ -3660,6 +3925,10 @@
"title": "Response", "title": "Response",
"type": "object" "type": "object"
}, },
"controllers.AuthForm": {
"title": "AuthForm",
"type": "object"
},
"controllers.EmailForm": { "controllers.EmailForm": {
"title": "EmailForm", "title": "EmailForm",
"type": "object", "type": "object",
@ -3684,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": { "controllers.Response": {
"title": "Response", "title": "Response",
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {
"$ref": "#/definitions/2306.0xc0003a4480.false" "$ref": "#/definitions/1183.0xc000455050.false"
}, },
"data2": { "data2": {
"$ref": "#/definitions/2340.0xc0003a44b0.false" "$ref": "#/definitions/1217.0xc000455080.false"
}, },
"msg": { "msg": {
"type": "string" "type": "string"
@ -4047,6 +4223,52 @@
} }
} }
}, },
"object.Chat": {
"title": "Chat",
"type": "object",
"properties": {
"category": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"displayName": {
"type": "string"
},
"messageCount": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"organization": {
"type": "string"
},
"owner": {
"type": "string"
},
"type": {
"type": "string"
},
"updatedTime": {
"type": "string"
},
"user1": {
"type": "string"
},
"user2": {
"type": "string"
},
"users": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"object.Header": { "object.Header": {
"title": "Header", "title": "Header",
"type": "object", "type": "object",
@ -4125,6 +4347,33 @@
} }
} }
}, },
"object.Message": {
"title": "Message",
"type": "object",
"properties": {
"author": {
"type": "string"
},
"chat": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"name": {
"type": "string"
},
"organization": {
"type": "string"
},
"owner": {
"type": "string"
},
"text": {
"type": "string"
}
}
},
"object.Model": { "object.Model": {
"title": "Model", "title": "Model",
"type": "object", "type": "object",

View File

@ -64,11 +64,47 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/add-chat:
post:
tags:
- Chat API
description: add chat
operationId: ApiController.AddChat
parameters:
- in: body
name: body
description: The details of the chat
required: true
schema:
$ref: '#/definitions/object.Chat'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-ldap: /api/add-ldap:
post: post:
tags: tags:
- Account API - Account API
operationId: ApiController.AddLdap operationId: ApiController.AddLdap
/api/add-message:
post:
tags:
- Message API
description: add message
operationId: ApiController.AddMessage
parameters:
- in: body
name: body
description: The details of the message
required: true
schema:
$ref: '#/definitions/object.Message'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-model: /api/add-model:
post: post:
tags: tags:
@ -319,6 +355,23 @@ paths:
tags: tags:
- Login API - Login API
operationId: ApiController.GetCaptcha 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: /api/api/get-webhook-event:
get: get:
tags: tags:
@ -385,6 +438,11 @@ paths:
description: object description: object
schema: schema:
$ref: '#/definitions/Response' $ref: '#/definitions/Response'
/api/api/verify-code:
post:
tags:
- Verification API
operationId: ApiController.VerifyCode
/api/api/webhook: /api/api/webhook:
post: post:
tags: tags:
@ -453,11 +511,47 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/delete-chat:
post:
tags:
- Chat API
description: delete chat
operationId: ApiController.DeleteChat
parameters:
- in: body
name: body
description: The details of the chat
required: true
schema:
$ref: '#/definitions/object.Chat'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-ldap: /api/delete-ldap:
post: post:
tags: tags:
- Account API - Account API
operationId: ApiController.DeleteLdap operationId: ApiController.DeleteLdap
/api/delete-message:
post:
tags:
- Message API
description: delete message
operationId: ApiController.DeleteMessage
parameters:
- in: body
name: body
description: The details of the message
required: true
schema:
$ref: '#/definitions/object.Message'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-model: /api/delete-model:
post: post:
tags: tags:
@ -800,6 +894,42 @@ paths:
type: array type: array
items: items:
$ref: '#/definitions/object.Cert' $ref: '#/definitions/object.Cert'
/api/get-chat:
get:
tags:
- Chat API
description: get chat
operationId: ApiController.GetChat
parameters:
- in: query
name: id
description: The id ( owner/name ) of the chat
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Chat'
/api/get-chats:
get:
tags:
- Chat API
description: get chats
operationId: ApiController.GetChats
parameters:
- in: query
name: owner
description: The owner of chats
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Chat'
/api/get-default-application: /api/get-default-application:
get: get:
tags: tags:
@ -880,6 +1010,42 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.GetLdaps operationId: ApiController.GetLdaps
/api/get-message:
get:
tags:
- Message API
description: get message
operationId: ApiController.GetMessage
parameters:
- in: query
name: id
description: The id ( owner/name ) of the message
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Message'
/api/get-messages:
get:
tags:
- Message API
description: get messages
operationId: ApiController.GetMessages
parameters:
- in: query
name: owner
description: The owner of messages
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Message'
/api/get-model: /api/get-model:
get: get:
tags: tags:
@ -1644,7 +1810,7 @@ paths:
description: Login information description: Login information
required: true required: true
schema: schema:
$ref: '#/definitions/controllers.RequestForm' $ref: '#/definitions/controllers.AuthForm'
responses: responses:
"200": "200":
description: The Response object description: The Response object
@ -1690,48 +1856,6 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/object.TokenError' $ref: '#/definitions/object.TokenError'
/api/login/oauth/code:
post:
tags:
- Token API
description: get OAuth code
operationId: ApiController.GetOAuthCode
parameters:
- in: query
name: id
description: The id ( owner/name ) of user
required: true
type: string
- in: query
name: client_id
description: OAuth client id
required: true
type: string
- in: query
name: response_type
description: OAuth response type
required: true
type: string
- in: query
name: redirect_uri
description: OAuth redirect URI
required: true
type: string
- in: query
name: scope
description: OAuth scope
required: true
type: string
- in: query
name: state
description: OAuth state
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.TokenWrapper'
/api/login/oauth/introspect: /api/login/oauth/introspect:
post: post:
description: The introspection endpoint is an OAuth 2.0 endpoint that takes a description: The introspection endpoint is an OAuth 2.0 endpoint that takes a
@ -2001,11 +2125,57 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/update-chat:
post:
tags:
- Chat API
description: update chat
operationId: ApiController.UpdateChat
parameters:
- in: query
name: id
description: The id ( owner/name ) of the chat
required: true
type: string
- in: body
name: body
description: The details of the chat
required: true
schema:
$ref: '#/definitions/object.Chat'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-ldap: /api/update-ldap:
post: post:
tags: tags:
- Account API - Account API
operationId: ApiController.UpdateLdap operationId: ApiController.UpdateLdap
/api/update-message:
post:
tags:
- Message API
description: update message
operationId: ApiController.UpdateMessage
parameters:
- in: query
name: id
description: The id ( owner/name ) of the message
required: true
type: string
- in: body
name: body
description: The details of the message
required: true
schema:
$ref: '#/definitions/object.Message'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-model: /api/update-model:
post: post:
tags: tags:
@ -2385,10 +2555,10 @@ paths:
schema: schema:
$ref: '#/definitions/Response' $ref: '#/definitions/Response'
definitions: definitions:
2306.0xc0003a4480.false: 1183.0xc000455050.false:
title: "false" title: "false"
type: object type: object
2340.0xc0003a44b0.false: 1217.0xc000455080.false:
title: "false" title: "false"
type: object type: object
LaravelResponse: LaravelResponse:
@ -2397,6 +2567,9 @@ definitions:
Response: Response:
title: Response title: Response
type: object type: object
controllers.AuthForm:
title: AuthForm
type: object
controllers.EmailForm: controllers.EmailForm:
title: EmailForm title: EmailForm
type: object type: object
@ -2413,76 +2586,14 @@ definitions:
type: string type: string
title: title:
type: string 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: controllers.Response:
title: Response title: Response
type: object type: object
properties: properties:
data: data:
$ref: '#/definitions/2306.0xc0003a4480.false' $ref: '#/definitions/1183.0xc000455050.false'
data2: data2:
$ref: '#/definitions/2340.0xc0003a44b0.false' $ref: '#/definitions/1217.0xc000455080.false'
msg: msg:
type: string type: string
name: name:
@ -2657,6 +2768,37 @@ definitions:
type: string type: string
type: type:
type: string type: string
object.Chat:
title: Chat
type: object
properties:
category:
type: string
createdTime:
type: string
displayName:
type: string
messageCount:
type: integer
format: int64
name:
type: string
organization:
type: string
owner:
type: string
type:
type: string
updatedTime:
type: string
user1:
type: string
user2:
type: string
users:
type: array
items:
type: string
object.Header: object.Header:
title: Header title: Header
type: object type: object
@ -2710,6 +2852,24 @@ definitions:
type: string type: string
username: username:
type: string type: string
object.Message:
title: Message
type: object
properties:
author:
type: string
chat:
type: string
createdTime:
type: string
name:
type: string
organization:
type: string
owner:
type: string
text:
type: string
object.Model: object.Model:
title: Model title: Model
type: object type: object

View File

@ -30,3 +30,12 @@ func ContainsString(values []string, val string) bool {
sort.Strings(values) sort.Strings(values)
return sort.SearchStrings(values, val) != len(values) return sort.SearchStrings(values, val) != len(values)
} }
func ReturnAnyNotEmpty(strs ...string) string {
for _, str := range strs {
if str != "" {
return str
}
}
return ""
}

View File

@ -95,6 +95,15 @@ func GetOwnerAndNameFromId(id string) (string, string) {
return tokens[0], tokens[1] return tokens[0], tokens[1]
} }
func GetOwnerFromId(id string) string {
tokens := strings.Split(id, "/")
if len(tokens) != 2 {
panic(errors.New("GetOwnerAndNameFromId() error, wrong token count for ID: " + id))
}
return tokens[0]
}
func GetOwnerAndNameFromIdNoCheck(id string) (string, string) { func GetOwnerAndNameFromIdNoCheck(id string) (string, string) {
tokens := strings.SplitN(id, "/", 2) tokens := strings.SplitN(id, "/", 2)
return tokens[0], tokens[1] return tokens[0], tokens[1]
@ -250,3 +259,11 @@ func maskString(str string) string {
return fmt.Sprintf("%c%s%c", str[0], strings.Repeat("*", len(str)-2), str[len(str)-1]) return fmt.Sprintf("%c%s%c", str[0], strings.Repeat("*", len(str)-2), str[len(str)-1])
} }
} }
// GetEndPoint remove scheme from url
func GetEndPoint(endpoint string) string {
for _, prefix := range []string{"https://", "http://"} {
endpoint = strings.TrimPrefix(endpoint, prefix)
}
return endpoint
}

View File

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "^1.5.6", "@ant-design/cssinjs": "^1.8.1",
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@craco/craco": "^6.4.5", "@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10", "@crowdin/cli": "^3.7.10",
@ -12,7 +12,7 @@
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
"antd": "5.1.6", "antd": "5.2.3",
"antd-token-previewer": "^1.1.0-22", "antd-token-previewer": "^1.1.0-22",
"codemirror": "^5.61.1", "codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
@ -76,7 +76,6 @@
"eslint-plugin-react": "^7.31.1", "eslint-plugin-react": "^7.31.1",
"husky": "^4.3.8", "husky": "^4.3.8",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"path-browserify": "^1.0.1",
"stylelint": "^14.11.0", "stylelint": "^14.11.0",
"stylelint-config-recommended-less": "^1.0.4", "stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^28.0.0" "stylelint-config-standard": "^28.0.0"

View File

@ -112,6 +112,7 @@ class AdapterEditPage extends React.Component {
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.organization} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.adapter.organization} onChange={(value => {
this.getModels(value); this.getModels(value);
this.updateAdapterField("organization", value); this.updateAdapterField("organization", value);
this.updateAdapterField("owner", value);
})}> })}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
@ -266,7 +267,7 @@ class AdapterEditPage extends React.Component {
submitAdapterEdit(willExist) { submitAdapterEdit(willExist) {
const adapter = Setting.deepCopy(this.state.adapter); const adapter = Setting.deepCopy(this.state.adapter);
AdapterBackend.updateAdapter(this.state.adapter.owner, this.state.adapterName, adapter) AdapterBackend.updateAdapter(this.state.owner, this.state.adapterName, adapter)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as AdapterBackend from "./backend/AdapterBackend"; import * as AdapterBackend from "./backend/AdapterBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class AdapterListPage extends BaseListPage { class AdapterListPage extends BaseListPage {
newAdapter() { newAdapter() {
@ -225,7 +225,7 @@ class AdapterListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={adapters} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={adapters} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Adapters")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Adapters")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -17,7 +17,7 @@ import "./App.less";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {BarsOutlined, DownOutlined, InfoCircleFilled, LogoutOutlined, SettingOutlined} from "@ant-design/icons"; import {BarsOutlined, CommentOutlined, DownOutlined, InfoCircleFilled, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
import {Alert, Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd"; import {Alert, Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom"; import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage"; import OrganizationListPage from "./OrganizationListPage";
@ -44,6 +44,11 @@ import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage"; import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage"; import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage"; import CertEditPage from "./CertEditPage";
import ChatListPage from "./ChatListPage";
import ChatEditPage from "./ChatEditPage";
import ChatPage from "./ChatPage";
import MessageEditPage from "./MessageEditPage";
import MessageListPage from "./MessageListPage";
import ProductListPage from "./ProductListPage"; import ProductListPage from "./ProductListPage";
import ProductEditPage from "./ProductEditPage"; import ProductEditPage from "./ProductEditPage";
import ProductBuyPage from "./ProductBuyPage"; import ProductBuyPage from "./ProductBuyPage";
@ -147,6 +152,10 @@ class App extends Component {
this.setState({selectedMenuKey: "/syncers"}); this.setState({selectedMenuKey: "/syncers"});
} else if (uri.includes("/certs")) { } else if (uri.includes("/certs")) {
this.setState({selectedMenuKey: "/certs"}); this.setState({selectedMenuKey: "/certs"});
} else if (uri.includes("/chats")) {
this.setState({selectedMenuKey: "/chats"});
} else if (uri.includes("/messages")) {
this.setState({selectedMenuKey: "/messages"});
} else if (uri.includes("/products")) { } else if (uri.includes("/products")) {
this.setState({selectedMenuKey: "/products"}); this.setState({selectedMenuKey: "/products"});
} else if (uri.includes("/payments")) { } else if (uri.includes("/payments")) {
@ -317,12 +326,17 @@ class App extends Component {
items.push(Setting.getItem(<><SettingOutlined />&nbsp;&nbsp;{i18next.t("account:My Account")}</>, items.push(Setting.getItem(<><SettingOutlined />&nbsp;&nbsp;{i18next.t("account:My Account")}</>,
"/account" "/account"
)); ));
items.push(Setting.getItem(<><CommentOutlined />&nbsp;&nbsp;{i18next.t("account:Chats & Messages")}</>,
"/chat"
));
items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Logout")}</>, items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Logout")}</>,
"/logout")); "/logout"));
const onClick = (e) => { const onClick = (e) => {
if (e.key === "/account") { if (e.key === "/account") {
this.props.history.push("/account"); this.props.history.push("/account");
} else if (e.key === "/chat") {
this.props.history.push("/chat");
} else if (e.key === "/logout") { } else if (e.key === "/logout") {
this.logout(); this.logout();
} }
@ -415,6 +429,14 @@ class App extends Component {
"/providers" "/providers"
)); ));
res.push(Setting.getItem(<Link to="/chats">{i18next.t("general:Chats")}</Link>,
"/chats"
));
res.push(Setting.getItem(<Link to="/messages">{i18next.t("general:Messages")}</Link>,
"/messages"
));
res.push(Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, res.push(Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>,
"/resources" "/resources"
)); ));
@ -529,6 +551,11 @@ class App extends Component {
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} /> <Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} /> <Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} /> <Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
<Route exact path="/chats" render={(props) => this.renderLoginIfNotLoggedIn(<ChatListPage account={this.state.account} {...props} />)} />
<Route exact path="/chats/:chatName" render={(props) => this.renderLoginIfNotLoggedIn(<ChatEditPage account={this.state.account} {...props} />)} />
<Route exact path="/chat" render={(props) => this.renderLoginIfNotLoggedIn(<ChatPage account={this.state.account} {...props} />)} />
<Route exact path="/messages" render={(props) => this.renderLoginIfNotLoggedIn(<MessageListPage account={this.state.account} {...props} />)} />
<Route exact path="/messages/:messageName" render={(props) => this.renderLoginIfNotLoggedIn(<MessageEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} /> <Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} /> <Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} /> <Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />
@ -602,7 +629,7 @@ class App extends Component {
} }
</Header> </Header>
<Content style={{display: "flex", flexDirection: "column"}} > <Content style={{display: "flex", flexDirection: "column"}} >
{Setting.isMobile() ? {(Setting.isMobile() || window.location.pathname === "/chat") ?
this.renderRouter() : this.renderRouter() :
<Card className="content-warp-card"> <Card className="content-warp-card">
{this.renderRouter()} {this.renderRouter()}

View File

@ -21,7 +21,7 @@ import * as Setting from "./Setting";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class ApplicationListPage extends BaseListPage { class ApplicationListPage extends BaseListPage {
constructor(props) { constructor(props) {
@ -254,7 +254,7 @@ class ApplicationListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={applications} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={applications} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Applications")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Applications")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

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

199
web/src/ChatBox.js Normal file
View File

@ -0,0 +1,199 @@
// 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 {Avatar, Input, List, Spin} from "antd";
import {CopyOutlined, DislikeOutlined, LikeOutlined, SendOutlined} from "@ant-design/icons";
import i18next from "i18next";
const {TextArea} = Input;
class ChatBox extends React.Component {
constructor(props) {
super(props);
this.state = {
inputValue: "",
};
this.listContainerRef = React.createRef();
}
componentDidUpdate(prevProps) {
if (prevProps.messages !== this.props.messages && this.props.messages !== null) {
this.scrollToListItem(this.props.messages.length);
}
}
handleKeyDown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (this.state.inputValue !== "") {
this.send(this.state.inputValue);
this.setState({inputValue: ""});
}
}
};
scrollToListItem(index) {
const listContainerElement = this.listContainerRef.current;
if (!listContainerElement) {
return;
}
const targetItem = listContainerElement.querySelector(
`#chatbox-list-item-${index}`
);
if (!targetItem) {
return;
}
const scrollDistance = targetItem.offsetTop - listContainerElement.offsetTop;
listContainerElement.scrollTo({
top: scrollDistance,
behavior: "smooth",
});
}
send = (text) => {
this.props.sendMessage(text);
this.setState({inputValue: ""});
};
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, {}]}
renderItem={(item, index) => {
if (Object.keys(item).length === 0 && item.constructor === Object) {
return <List.Item id={`chatbox-list-item-${index}`} style={{
height: "160px",
backgroundColor: index % 2 === 0 ? "white" : "rgb(247,247,248)",
borderBottom: "1px solid rgb(229, 229, 229)",
position: "relative",
}} />;
}
return (
<List.Item id={`chatbox-list-item-${index}`} style={{
backgroundColor: index % 2 === 0 ? "white" : "rgb(247,247,248)",
borderBottom: "1px solid rgb(229, 229, 229)",
position: "relative",
}}>
<div style={{width: "800px", margin: "0 auto", position: "relative"}}>
<List.Item.Meta
avatar={<Avatar style={{width: "30px", height: "30px", borderRadius: "3px"}} src={item.author === `${this.props.account.owner}/${this.props.account.name}` ? this.props.account.avatar : "https://cdn.casbin.com/casdoor/resource/built-in/admin/gpt.png"} />}
title={<div style={{fontSize: "16px", fontWeight: "normal", lineHeight: "24px", marginTop: "-15px", marginLeft: "5px", marginRight: "80px"}}>{item.text}</div>}
/>
<div style={{position: "absolute", top: "0px", right: "0px"}}
>
<CopyOutlined style={{color: "rgb(172,172,190)", margin: "5px"}} />
<LikeOutlined style={{color: "rgb(172,172,190)", margin: "5px"}} />
<DislikeOutlined style={{color: "rgb(172,172,190)", margin: "5px"}} />
</div>
</div>
</List.Item>
);
}}
/>
<div style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "120px",
background: "linear-gradient(transparent 0%, rgba(255, 255, 255, 0.8) 50%, white 100%)",
pointerEvents: "none",
}} />
</div>
);
}
renderInput() {
return (
<div
style={{
position: "fixed",
bottom: "90px",
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<div style={{position: "relative", width: "760px", marginLeft: "-280px"}}>
<TextArea
placeholder={"Send a message..."}
autoSize={{maxRows: 8}}
value={this.state.inputValue}
onChange={(e) => this.setState({inputValue: e.target.value})}
onKeyDown={this.handleKeyDown}
style={{
fontSize: "16px",
fontWeight: "normal",
lineHeight: "24px",
width: "770px",
height: "48px",
borderRadius: "6px",
borderColor: "rgb(229,229,229)",
boxShadow: "0 0 15px rgba(0, 0, 0, 0.1)",
paddingLeft: "17px",
paddingRight: "17px",
paddingTop: "12px",
paddingBottom: "12px",
}}
suffix={<SendOutlined style={{color: "rgb(210,210,217"}} onClick={() => this.send(this.state.inputValue)} />}
autoComplete="off"
/>
<SendOutlined
style={{
color: this.state.inputValue === "" ? "rgb(210,210,217)" : "rgb(142,142,160)",
position: "absolute",
bottom: "17px",
right: "17px",
}}
onClick={() => this.send(this.state.inputValue)}
/>
</div>
</div>
);
}
render() {
return (
<div>
{
this.renderList()
}
{
this.renderInput()
}
</div>
);
}
}
export default ChatBox;

243
web/src/ChatEditPage.js Normal file
View File

@ -0,0 +1,243 @@
// 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 {Button, Card, Col, Input, Row, Select} from "antd";
import * as ChatBackend from "./backend/ChatBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
class ChatEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
chatName: props.match.params.chatName,
chat: null,
organizations: [],
users: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getChat();
this.getOrganizations();
}
getChat() {
ChatBackend.getChat("admin", this.state.chatName)
.then((chat) => {
this.setState({
chat: chat,
});
this.getUsers(chat.organization);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
this.setState({
users: res,
});
});
}
parseChatField(key, value) {
if ([].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateChatField(key, value) {
value = this.parseChatField(key, value);
const chat = this.state.chat;
chat[key] = value;
this.setState({
chat: chat,
});
}
renderChat() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("chat:New Chat") : i18next.t("chat:Edit Chat")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitChatEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitChatEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteChat()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.chat.organization} onChange={(value => {this.updateChatField("organization", value);})}
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.chat.name} onChange={e => {
this.updateChatField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.chat.displayName} onChange={e => {
this.updateChatField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.chat.type} onChange={(value => {
this.updateChatField("type", value);
})}
options={[
{value: "Single", name: i18next.t("chat:Single")},
{value: "Group", name: i18next.t("chat:Group")},
{value: "AI", name: i18next.t("chat:AI")},
].map((item) => Setting.getOption(item.name, item.value))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider:Category - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.chat.category} onChange={e => {
this.updateChatField("category", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("chat:User1"), i18next.t("chat:User1 - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.chat.user1} onChange={(value => {this.updateChatField("user1", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("chat:User2"), i18next.t("chat:User2 - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.chat.user2} onChange={(value => {this.updateChatField("user2", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Users"), i18next.t("chat:Users - Tooltip"))} :
</Col>
<Col span={22} >
<Select mode="tags" style={{width: "100%"}} value={this.state.chat.users}
onChange={(value => {this.updateChatField("users", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
/>
</Col>
</Row>
</Card>
);
}
submitChatEdit(willExist) {
const chat = Setting.deepCopy(this.state.chat);
ChatBackend.updateChat(this.state.chat.owner, this.state.chatName, chat)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
chatName: this.state.chat.name,
});
if (willExist) {
this.props.history.push("/chats");
} else {
this.props.history.push(`/chats/${this.state.chat.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateChatField("name", this.state.chatName);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteChat() {
ChatBackend.deleteChat(this.state.chat)
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/chats");
} 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}`);
});
}
render() {
return (
<div>
{
this.state.chat !== null ? this.renderChat() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitChatEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitChatEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteChat()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default ChatEditPage;

294
web/src/ChatListPage.js Normal file
View File

@ -0,0 +1,294 @@
// 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 {Link} from "react-router-dom";
import {Button, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as ChatBackend from "./backend/ChatBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ChatListPage extends BaseListPage {
newChat() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.applicationName,
name: `chat_${randomName}`,
createdTime: moment().format(),
updatedTime: moment().format(),
organization: this.props.account.owner,
displayName: `New Chat - ${randomName}`,
type: "Single",
category: "Chat Category - 1",
user1: `${this.props.account.owner}/${this.props.account.name}`,
user2: "",
users: [`${this.props.account.owner}/${this.props.account.name}`],
messageCount: 0,
};
}
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}`);
});
}
renderTable(chats) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "150px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/chats/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Updated time"),
dataIndex: "updatedTime",
key: "updatedTime",
width: "15 0px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("provider:Type"),
dataIndex: "type",
key: "type",
width: "110px",
sorter: true,
filterMultiple: false,
filters: [
{text: "Single", value: "Single"},
{text: "Group", value: "Group"},
{text: "AI", value: "AI"},
],
render: (text, record, index) => {
return i18next.t(`chat:${text}`);
},
},
{
title: i18next.t("provider:Category"),
dataIndex: "category",
key: "category",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("category"),
},
{
title: i18next.t("chat:User1"),
dataIndex: "user1",
key: "user1",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("user1"),
render: (text, record, index) => {
return (
<Link to={`/users/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("chat:User2"),
dataIndex: "user2",
key: "user2",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("user2"),
render: (text, record, index) => {
return (
<Link to={`/users/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Users"),
dataIndex: "users",
key: "users",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("users"),
render: (text, record, index) => {
return Setting.getTags(text, "users");
},
},
{
title: i18next.t("chat:Message count"),
dataIndex: "messageCount",
key: "messageCount",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("messageCount"),
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/chats/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteChat(index)}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={chats} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Chats")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addChat.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
ChatBackend.getChats("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
loading: false,
isAuthorized: false,
});
}
}
});
};
}
export default ChatListPage;

167
web/src/ChatMenu.js Normal file
View File

@ -0,0 +1,167 @@
// 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 {Button, Menu} from "antd";
import {DeleteOutlined, LayoutOutlined, PlusOutlined} from "@ant-design/icons";
class ChatMenu extends React.Component {
constructor(props) {
super(props);
const items = this.chatsToItems(this.props.chats);
const openKeys = items.map((item) => item.key);
this.state = {
openKeys: openKeys,
selectedKeys: ["0-0"],
};
}
chatsToItems(chats) {
const categories = {};
chats.forEach((chat) => {
if (!categories[chat.category]) {
categories[chat.category] = [];
}
categories[chat.category].push(chat);
});
const selectedKeys = this.state === undefined ? [] : this.state.selectedKeys;
return Object.keys(categories).map((category, index) => {
return {
key: `${index}`,
icon: <LayoutOutlined />,
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: (
<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>
),
};
}),
};
});
}
onSelect = (info) => {
const [categoryIndex, chatIndex] = info.selectedKeys[0].split("-").map(Number);
const selectedItem = this.chatsToItems(this.props.chats)[categoryIndex].children[chatIndex];
this.setState({selectedKeys: [`${categoryIndex}-${chatIndex}`]});
if (this.props.onSelectChat) {
this.props.onSelectChat(selectedItem.index);
}
};
getRootSubmenuKeys(items) {
return items.map((item, index) => `${index}`);
}
onOpenChange = (keys) => {
const items = this.chatsToItems(this.props.chats);
const rootSubmenuKeys = this.getRootSubmenuKeys(items);
const latestOpenKey = keys.find((key) => this.state.openKeys.indexOf(key) === -1);
if (rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
this.setState({openKeys: keys});
} else {
this.setState({openKeys: latestOpenKey ? [latestOpenKey] : []});
}
};
render() {
const items = this.chatsToItems(this.props.chats);
return (
<>
<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}
/>
</>
);
}
}
export default ChatMenu;

242
web/src/ChatPage.js Normal file
View File

@ -0,0 +1,242 @@
// 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 {Spin} from "antd";
import moment from "moment";
import ChatMenu from "./ChatMenu";
import ChatBox from "./ChatBox";
import * as Setting from "./Setting";
import * as ChatBackend from "./backend/ChatBackend";
import * as MessageBackend from "./backend/MessageBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ChatPage extends BaseListPage {
newChat(chat) {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.applicationName,
name: `chat_${randomName}`,
createdTime: moment().format(),
updatedTime: moment().format(),
organization: this.props.account.owner,
displayName: `New Chat - ${randomName}`,
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}`],
messageCount: 0,
};
}
newMessage(text) {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.messagename,
name: `message_${randomName}`,
createdTime: moment().format(),
organization: this.props.account.owner,
chat: this.state.chatName,
author: `${this.props.account.owner}/${this.props.account.name}`,
text: text,
};
}
sendMessage(text) {
const newMessage = this.newMessage(text);
MessageBackend.addMessage(newMessage)
.then((res) => {
if (res.status === "ok") {
this.getMessages(this.state.chatName);
} 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}`);
});
}
getMessages(chatName) {
MessageBackend.getChatMessages(chatName)
.then((messages) => {
this.setState({
messages: messages,
});
Setting.scrollToDiv(`chatbox-list-item-${messages.length}`);
});
}
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) {
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>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
ChatBackend.getChats("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
const chats = res.data;
if (this.state.chatName === undefined && chats.length > 0) {
const chat = chats[0];
this.getMessages(chat.name);
this.setState({
chatName: chat.name,
});
}
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
loading: false,
isAuthorized: false,
});
}
}
});
};
}
export default ChatPage;

View File

@ -25,6 +25,7 @@ import SelfForgetPage from "./auth/SelfForgetPage";
import ForgetPage from "./auth/ForgetPage"; import ForgetPage from "./auth/ForgetPage";
import PromptPage from "./auth/PromptPage"; import PromptPage from "./auth/PromptPage";
import CasLogout from "./auth/CasLogout"; import CasLogout from "./auth/CasLogout";
import {authConfig} from "./auth/Auth";
class EntryPage extends React.Component { class EntryPage extends React.Component {
constructor(props) { constructor(props) {
@ -69,9 +70,9 @@ class EntryPage extends React.Component {
return ( return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}> <div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
<Spin spinning={this.state.application === undefined} tip={i18next.t("login:Loading")} style={{margin: "0 auto"}} /> <Spin size="large" spinning={this.state.application === undefined} tip={i18next.t("login:Loading")} style={{margin: "0 auto"}} />
<Switch> <Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
@ -84,7 +85,7 @@ class EntryPage extends React.Component {
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signup"} onUpdateApplication={onUpdateApplication} {...props} />);}} /> <Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} />
</Switch> </Switch>
</div> </div>
); );

View File

@ -166,13 +166,37 @@ class LdapEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Search Filter"), i18next.t("ldap:Search Filter - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.filter} onChange={e => {
this.updateLdapField("filter", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Filter fields"), i18next.t("ldap:Filter fields - Tooltip"))} :
</Col>
<Col span={21}>
<Select value={this.state.ldap.filterFields ?? []} style={{width: "100%"}} mode={"multiple"} options={[
{value: "uid", label: "uid"},
{value: "mail", label: "Email"},
{value: "mobile", label: "mobile"},
].map((item) => Setting.getOption(item.label, item.value))} onChange={value => {
this.updateLdapField("filterFields", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}> <Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}> <Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} : {Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
</Col> </Col>
<Col span={21}> <Col span={21}>
<Input value={this.state.ldap.admin} onChange={e => { <Input value={this.state.ldap.username} onChange={e => {
this.updateLdapField("admin", e.target.value); this.updateLdapField("username", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
@ -182,9 +206,9 @@ class LdapEditPage extends React.Component {
</Col> </Col>
<Col span={21}> <Col span={21}>
<Input.Password <Input.Password
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)} value={this.state.ldap.passwd} iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)} value={this.state.ldap.password}
onChange={e => { onChange={e => {
this.updateLdapField("passwd", e.target.value); this.updateLdapField("password", e.target.value);
}} }}
/> />
</Col> </Col>

View File

@ -1,192 +0,0 @@
// Copyright 2021 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 {Link} from "react-router-dom";
import {Button, Col, Row, Table} from "antd";
import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next";
import PopconfirmModal from "./PopconfirmModal";
class LdapListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ldaps: null,
};
}
UNSAFE_componentWillMount() {
this.getLdaps();
}
getLdaps() {
LdapBackend.getLdaps("")
.then((res) => {
let ldapsData = [];
if (res.status === "ok") {
ldapsData = res.data;
} else {
Setting.showMessage("error", res.msg);
}
this.setState((prevState) => {
prevState.ldaps = ldapsData;
return prevState;
});
});
}
deleteLdap(index) {
}
renderTable(ldaps) {
const columns = [
{
title: i18next.t("ldap:Server name"),
dataIndex: "serverName",
key: "serverName",
width: "200px",
sorter: (a, b) => a.serverName.localeCompare(b.serverName),
render: (text, record, index) => {
return (
<Link to={`/ldaps/${record.id}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "140px",
sorter: (a, b) => a.owner.localeCompare(b.owner),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("ldap:Server"),
dataIndex: "host",
key: "host",
ellipsis: true,
sorter: (a, b) => a.host.localeCompare(b.host),
render: (text, record, index) => {
return `${text}:${record.port}`;
},
},
{
title: i18next.t("ldap:Base DN"),
dataIndex: "baseDn",
key: "baseDn",
ellipsis: true,
sorter: (a, b) => a.baseDn.localeCompare(b.baseDn),
},
{
title: i18next.t("ldap:Admin"),
dataIndex: "admin",
key: "admin",
ellipsis: true,
sorter: (a, b) => a.admin.localeCompare(b.admin),
},
{
title: i18next.t("ldap:Auto Sync"),
dataIndex: "autoSync",
key: "autoSync",
width: "100px",
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
render: (text, record, index) => {
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
<span style={{color: "#52c41a"}}>{text + " mins"}</span>);
},
},
{
title: i18next.t("ldap:Last Sync"),
dataIndex: "lastSync",
key: "lastSync",
ellipsis: true,
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
render: (text, record, index) => {
return text;
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "240px",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
type="primary"
onClick={() => Setting.goToLink(`/ldap/sync/${record.id}`)}>{i18next.t("general:Sync")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
onClick={() => Setting.goToLink(`/ldap/${record.id}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.serverName} ?`}
onConfirm={() => this.deleteLdap(index)}
>
</PopconfirmModal>
</div>
);
},
},
];
return (
<div>
<Table columns={columns} dataSource={ldaps} rowKey="id" size="middle" bordered
pagination={{pageSize: 100}}
title={() => (
<div>
<span>{i18next.t("general:LDAPs")}</span>
<Button type="primary" size="small" style={{marginLeft: "10px"}}
onClick={() => {
this.addLdap();
}}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={ldaps === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.ldaps)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default LdapListPage;

View File

@ -13,10 +13,11 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Col, Popconfirm, Row, Table} from "antd"; import {Button, Popconfirm, Table} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend"; import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next"; import i18next from "i18next";
import {Link} from "react-router-dom";
class LdapSyncPage extends React.Component { class LdapSyncPage extends React.Component {
constructor(props) { constructor(props) {
@ -77,9 +78,8 @@ class LdapSyncPage extends React.Component {
LdapBackend.getLdap(this.state.organizationName, this.state.ldapId) LdapBackend.getLdap(this.state.organizationName, this.state.ldapId)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.setState((prevState) => { this.setState({
prevState.ldap = res.data; ldap: res.data,
return prevState;
}); });
this.getLdapUser(); this.getLdapUser();
} else { } else {
@ -139,22 +139,46 @@ class LdapSyncPage extends React.Component {
dataIndex: "cn", dataIndex: "cn",
key: "cn", key: "cn",
sorter: (a, b) => a.cn.localeCompare(b.cn), sorter: (a, b) => a.cn.localeCompare(b.cn),
render: (text, record, index) => {
return (<div style={{display: "flex", justifyContent: "space-between"}}>
<div>
{text}
</div>
{this.state.existUuids.includes(record.uuid) ?
Setting.getTag("green", i18next.t("ldap:synced")) :
Setting.getTag("red", i18next.t("ldap:unsynced"))
}
</div>);
},
}, },
{ {
title: i18next.t("ldap:UidNumber / Uid"), title: "Uid",
dataIndex: "uid",
key: "uid",
sorter: (a, b) => a.uid.localeCompare(b.uid),
render: (text, record, index) => {
return (
this.state.existUuids.includes(record.uuid) ?
<Link to={`/users/${this.state.organizationName}/${text}`}>
{text}
</Link> :
text
);
},
},
{
title: "UidNumber",
dataIndex: "uidNumber", dataIndex: "uidNumber",
key: "uidNumber", key: "uidNumber",
width: "200px",
sorter: (a, b) => a.uidNumber.localeCompare(b.uidNumber), sorter: (a, b) => a.uidNumber.localeCompare(b.uidNumber),
render: (text, record, index) => { render: (text, record, index) => {
return `${text} / ${record.uid}`; return text;
}, },
}, },
{ {
title: i18next.t("ldap:Group ID"), title: i18next.t("ldap:Group ID"),
dataIndex: "groupId", dataIndex: "groupId",
key: "groupId", key: "groupId",
width: "140px",
sorter: (a, b) => a.groupId.localeCompare(b.groupId), sorter: (a, b) => a.groupId.localeCompare(b.groupId),
filters: this.buildFilter(this.state.users, "groupId"), filters: this.buildFilter(this.state.users, "groupId"),
onFilter: (value, record) => record.groupId.indexOf(value) === 0, onFilter: (value, record) => record.groupId.indexOf(value) === 0,
@ -163,14 +187,12 @@ class LdapSyncPage extends React.Component {
title: i18next.t("general:Email"), title: i18next.t("general:Email"),
dataIndex: "email", dataIndex: "email",
key: "email", key: "email",
width: "240px",
sorter: (a, b) => a.email.localeCompare(b.email), sorter: (a, b) => a.email.localeCompare(b.email),
}, },
{ {
title: i18next.t("general:Phone"), title: i18next.t("general:Phone"),
dataIndex: "phone", dataIndex: "phone",
key: "phone", key: "phone",
width: "160px",
sorter: (a, b) => a.phone.localeCompare(b.phone), sorter: (a, b) => a.phone.localeCompare(b.phone),
}, },
{ {
@ -183,9 +205,8 @@ class LdapSyncPage extends React.Component {
const rowSelection = { const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => { onChange: (selectedRowKeys, selectedRows) => {
this.setState(prevState => { this.setState({
prevState.selectedUsers = selectedRows; selectedUsers: selectedRows,
return prevState;
}); });
}, },
getCheckboxProps: record => ({ getCheckboxProps: record => ({
@ -194,42 +215,36 @@ class LdapSyncPage extends React.Component {
}; };
return ( return (
<div> <Table rowSelection={rowSelection} columns={columns} dataSource={users} rowKey="uuid" bordered size="small"
<Table rowSelection={rowSelection} columns={columns} dataSource={users} rowKey="uuid" bordered pagination={{defaultPageSize: 10, showQuickJumper: true, showSizeChanger: true}}
pagination={{defaultPageSize: 10, showQuickJumper: true, showSizeChanger: true}} title={() => (
title={() => ( <div>
<div> {this.state.ldap?.serverName}
<span>{this.state.ldap?.serverName}</span> <Popconfirm placement={"right"} disabled={this.state.selectedUsers.length === 0}
<Popconfirm placement={"right"} title={"Please confirm to sync selected users"}
title={"Please confirm to sync selected users"} onConfirm={() => this.syncUsers()}
onConfirm={() => this.syncUsers()} >
> <Button type="primary" style={{marginLeft: "10px"}} disabled={this.state.selectedUsers.length === 0}>
<Button type="primary" style={{marginLeft: "10px"}}> {i18next.t("general:Sync")}
{i18next.t("general:Sync")}
</Button>
</Popconfirm>
<Button style={{marginLeft: "20px"}}
onClick={() => Setting.goToLink(`/ldap/${this.state.organizationName}/${this.state.ldapId}`)}>
{i18next.t("general:Edit")} LDAP
</Button> </Button>
</div> </Popconfirm>
)} <Button style={{marginLeft: "20px"}}
loading={users === null} onClick={() => Setting.goToLink(`/ldap/${this.state.organizationName}/${this.state.ldapId}`)}>
/> {i18next.t("general:Edit")} LDAP
</div> </Button>
</div>
)}
loading={users === null}
/>
); );
} }
render() { render() {
return ( return (
<div> <div>
<Row style={{width: "100%", justifyContent: "center"}}> {
<Col span={22}> this.renderTable(this.state.users)
{ }
this.renderTable(this.state.users)
}
</Col>
</Row>
<div style={{marginTop: "20px", marginLeft: "40px"}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => { <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => {
this.props.history.push(`/organizations/${this.state.organizationName}`); this.props.history.push(`/organizations/${this.state.organizationName}`);

220
web/src/MessageEditPage.js Normal file
View File

@ -0,0 +1,220 @@
// 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 {Button, Card, Col, Input, Row, Select} from "antd";
import * as ChatBackend from "./backend/ChatBackend";
import * as MessageBackend from "./backend/MessageBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
const {TextArea} = Input;
class MessageEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
messageName: props.match.params.messageName,
message: null,
organizations: [],
chats: [],
users: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getMessage();
this.getOrganizations();
this.getChats();
}
getMessage() {
MessageBackend.getMessage("admin", this.state.messageName)
.then((message) => {
this.setState({
message: message,
});
this.getUsers(message.organization);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getChats() {
ChatBackend.getChats("admin")
.then((res) => {
this.setState({
chats: (res.msg === undefined) ? res : [],
});
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
this.setState({
users: res,
});
});
}
parseMessageField(key, value) {
if ([].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateMessageField(key, value) {
value = this.parseMessageField(key, value);
const message = this.state.message;
message[key] = value;
this.setState({
message: message,
});
}
renderMessage() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("message:New Message") : i18next.t("message:Edit Message")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitMessageEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitMessageEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteMessage()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.message.organization} onChange={(value => {this.updateMessageField("organization", value);})}
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.message.name} onChange={e => {
this.updateMessageField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("message:Chat"), i18next.t("message:Chat - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.message.chat} onChange={(value => {this.updateMessageField("chat", value);})}
options={this.state.chats.map((chat) => Setting.getOption(chat.name, chat.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("message:Author"), i18next.t("message:Author - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.message.author} onChange={(value => {this.updateMessageField("author", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("message:Text"), i18next.t("message:Text - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={10} value={this.state.message.text} onChange={e => {
this.updateMessageField("text", e.target.value);
}} />
</Col>
</Row>
</Card>
);
}
submitMessageEdit(willExist) {
const message = Setting.deepCopy(this.state.message);
MessageBackend.updateMessage(this.state.message.owner, this.state.messageName, message)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
messageName: this.state.message.name,
});
if (willExist) {
this.props.history.push("/messages");
} else {
this.props.history.push(`/messages/${this.state.message.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateMessageField("name", this.state.messageName);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteMessage() {
MessageBackend.deleteMessage(this.state.message)
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/messages");
} 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}`);
});
}
render() {
return (
<div>
{
this.state.message !== null ? this.renderMessage() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitMessageEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitMessageEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteMessage()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default MessageEditPage;

236
web/src/MessageListPage.js Normal file
View File

@ -0,0 +1,236 @@
// 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 {Link} from "react-router-dom";
import {Button, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as MessageBackend from "./backend/MessageBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class MessageListPage extends BaseListPage {
newMessage() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.messagename,
name: `message_${randomName}`,
createdTime: moment().format(),
organization: this.props.account.owner,
chat: "",
author: `${this.props.account.owner}/${this.props.account.name}`,
text: "",
};
}
addMessage() {
const newMessage = this.newMessage();
MessageBackend.addMessage(newMessage)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/messages/${newMessage.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}`);
});
}
deleteMessage(i) {
MessageBackend.deleteMessage(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}`);
});
}
renderTable(messages) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "150px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/messages/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("message:Chat"),
dataIndex: "chat",
key: "chat",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("chat"),
render: (text, record, index) => {
return (
<Link to={`/chats/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("message:Author"),
dataIndex: "author",
key: "author",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("author"),
render: (text, record, index) => {
return (
<Link to={`/users/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("message:Text"),
dataIndex: "text",
key: "text",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("text"),
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/messages/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteMessage(index)}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={messages} rowKey={(record) => `${record.owner}/${record.name}`}size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Messages")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addMessage.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
MessageBackend.getMessages("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
loading: false,
isAuthorized: false,
});
}
}
});
};
}
export default MessageListPage;

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as ModelBackend from "./backend/ModelBackend"; import * as ModelBackend from "./backend/ModelBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class ModelListPage extends BaseListPage { class ModelListPage extends BaseListPage {
newModel() { newModel() {
@ -163,7 +163,7 @@ class ModelListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={models} rowKey="name" size="middle" bordered <Table scroll={{x: "max-content"}} columns={columns} dataSource={models} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered
pagination={paginationProps} pagination={paginationProps}
title={() => ( title={() => (
<div> <div>

View File

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

View File

@ -21,7 +21,7 @@ import * as PaymentBackend from "./backend/PaymentBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import * as Provider from "./auth/Provider"; import * as Provider from "./auth/Provider";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class PaymentListPage extends BaseListPage { class PaymentListPage extends BaseListPage {
newPayment() { newPayment() {
@ -243,7 +243,7 @@ class PaymentListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={payments} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={payments} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Payments")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Payments")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as PermissionBackend from "./backend/PermissionBackend"; import * as PermissionBackend from "./backend/PermissionBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class PermissionListPage extends BaseListPage { class PermissionListPage extends BaseListPage {
newPermission() { newPermission() {
@ -139,7 +139,7 @@ class PermissionListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("users"), ...this.getColumnSearchProps("users"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text, "users");
}, },
}, },
{ {
@ -150,7 +150,7 @@ class PermissionListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("roles"), ...this.getColumnSearchProps("roles"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text, "roles");
}, },
}, },
{ {
@ -321,7 +321,7 @@ class PermissionListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={permissions} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={permissions} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

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

View File

@ -125,6 +125,8 @@ class ProviderEditPage extends React.Component {
} else { } else {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip")); 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: default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); 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"); this.updateProviderField("type", "Alipay");
} else if (value === "Captcha") { } else if (value === "Captcha") {
this.updateProviderField("type", "Default"); 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: "Email", name: "Email"},
{id: "OAuth", name: "OAuth"},
{id: "Payment", name: "Payment"},
{id: "SAML", name: "SAML"},
{id: "SMS", name: "SMS"}, {id: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"}, {id: "Storage", name: "Storage"},
{id: "SAML", name: "SAML"},
{id: "Payment", name: "Payment"},
{id: "Captcha", name: "Captcha"},
] ]
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>) .map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
@ -352,7 +357,7 @@ class ProviderEditPage extends React.Component {
[ [
{id: "Normal", name: i18next.t("provider:Normal")}, {id: "Normal", name: i18next.t("provider:Normal")},
{id: "Silent", name: i18next.t("provider:Silent")}, {id: "Silent", name: i18next.t("provider:Silent")},
].map((method, index) => <Option key={index} value={method.name}>{method.name}</Option>) ].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
@ -437,16 +442,20 @@ 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" ? null : (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> this.state.provider.category === "AI" ? null : (
{this.getClientIdLabel(this.state.provider)} <Row style={{marginTop: "20px"}} >
</Col> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
<Col span={22} > {this.getClientIdLabel(this.state.provider)}
<Input value={this.state.provider.clientId} onChange={e => { </Col>
this.updateProviderField("clientId", e.target.value); <Col span={22} >
}} /> <Input value={this.state.provider.clientId} onChange={e => {
</Col> this.updateProviderField("clientId", e.target.value);
</Row> }} />
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecretLabel(this.state.provider)} {this.getClientSecretLabel(this.state.provider)}
@ -461,13 +470,15 @@ class ProviderEditPage extends React.Component {
) )
} }
{ {
this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" ? null : ( this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" ? null : (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.state.provider.type === "Aliyun Captcha" {this.state.provider.type === "Aliyun Captcha"
? Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip")) ? Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"))
: Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))} : this.state.provider.type === "WeChat Pay"
? Setting.getLabel("appId", "appId")
: Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))}
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.provider.clientId2} onChange={e => { <Input value={this.state.provider.clientId2} onChange={e => {
@ -475,18 +486,22 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> this.state.provider.type === "WeChat Pay" ? null : (
{this.state.provider.type === "Aliyun Captcha" <Row style={{marginTop: "20px"}} >
? Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip")) <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
: Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"))} {this.state.provider.type === "Aliyun Captcha"
</Col> ? Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"))
<Col span={22} > : Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"))}
<Input value={this.state.provider.clientSecret2} onChange={e => { </Col>
this.updateProviderField("clientSecret2", e.target.value); <Col span={22} >
}} /> <Input value={this.state.provider.clientSecret2} onChange={e => {
</Col> this.updateProviderField("clientSecret2", e.target.value);
</Row> }} />
</Col>
</Row>
)
}
</React.Fragment> </React.Fragment>
) )
} }
@ -815,6 +830,20 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) : null ) : null
} }
{
this.state.provider.type === "WeChat Pay" ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("cert", "cert")} :
</Col>
<Col span={22} >
<Input value={this.state.provider.cert} onChange={e => {
this.updateProviderField("cert", e.target.value);
}} />
</Col>
</Row>
) : null
}
{this.getAppIdRow(this.state.provider)} {this.getAppIdRow(this.state.provider)}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>

View File

@ -21,7 +21,7 @@ import * as ProviderBackend from "./backend/ProviderBackend";
import * as Provider from "./auth/Provider"; import * as Provider from "./auth/Provider";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class ProviderListPage extends BaseListPage { class ProviderListPage extends BaseListPage {
constructor(props) { constructor(props) {
@ -227,7 +227,7 @@ class ProviderListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={providers} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={providers} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Providers")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Providers")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

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

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as RoleBackend from "./backend/RoleBackend"; import * as RoleBackend from "./backend/RoleBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class RoleListPage extends BaseListPage { class RoleListPage extends BaseListPage {
newRole() { newRole() {
@ -130,7 +130,7 @@ class RoleListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("users"), ...this.getColumnSearchProps("users"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text, "users");
}, },
}, },
{ {
@ -141,7 +141,7 @@ class RoleListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("roles"), ...this.getColumnSearchProps("roles"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text, "roles");
}, },
}, },
{ {
@ -196,7 +196,7 @@ class RoleListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={roles} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={roles} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Roles")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Roles")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -19,7 +19,7 @@ import {Link} from "react-router-dom";
import {Table, Tag} from "antd"; import {Table, Tag} from "antd";
import React from "react"; import React from "react";
import * as SessionBackend from "./backend/SessionBackend"; import * as SessionBackend from "./backend/SessionBackend";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class SessionListPage extends BaseListPage { class SessionListPage extends BaseListPage {
@ -118,7 +118,7 @@ class SessionListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={sessions} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={sessions} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
loading={this.state.loading} loading={this.state.loading}
onChange={this.handleTableChange} onChange={this.handleTableChange}
/> />

View File

@ -24,7 +24,6 @@ import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import * as Conf from "./Conf"; import * as Conf from "./Conf";
import * as phoneNumber from "libphonenumber-js"; import * as phoneNumber from "libphonenumber-js";
import * as path from "path-browserify";
const {Option} = Select; const {Option} = Select;
@ -42,7 +41,7 @@ export const Countries = [{label: "English", key: "en", country: "US", alt: "Eng
{label: "日本語", key: "ja", country: "JP", alt: "日本語"}, {label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"}, {label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"}, {label: "Русский", key: "ru", country: "RU", alt: "Русский"},
{label: "TiếngViệt", key: "vi", country: "VI", alt: "TiếngViệt"}, {label: "TiếngViệt", key: "vi", country: "VN", alt: "TiếngViệt"},
]; ];
export function getThemeData(organization, application) { export function getThemeData(organization, application) {
@ -205,6 +204,12 @@ export const OtherProviderInfo = {
url: "https://www.cloudflare.com/products/turnstile/", 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() { export function initCountries() {
@ -856,6 +861,10 @@ export function getProviderTypeOptions(category) {
{id: "GEETEST", name: "GEETEST"}, {id: "GEETEST", name: "GEETEST"},
{id: "Cloudflare Turnstile", name: "Cloudflare Turnstile"}, {id: "Cloudflare Turnstile", name: "Cloudflare Turnstile"},
]); ]);
} else if (category === "AI") {
return ([
{id: "OpenAI API - GPT", name: "OpenAI API - GPT"},
]);
} else { } else {
return []; return [];
} }
@ -888,7 +897,7 @@ export function getLoginLink(application) {
} else if (authConfig.appName === application.name) { } else if (authConfig.appName === application.name) {
url = "/login"; url = "/login";
} else if (application.signinUrl === "") { } else if (application.signinUrl === "") {
url = path.join(application.homepageUrl, "/login"); url = trim(application.homepageUrl, "/") + "/login";
} else { } else {
url = application.signinUrl; url = application.signinUrl;
} }
@ -902,10 +911,11 @@ export function renderLoginLink(application, text) {
export function redirectToLoginPage(application, history) { export function redirectToLoginPage(application, history) {
const loginLink = getLoginLink(application); const loginLink = getLoginLink(application);
if (loginLink.indexOf("http") === 0 || loginLink.indexOf("https") === 0) { if (loginLink.startsWith("http://") || loginLink.startsWith("https://")) {
window.location.replace(loginLink); goToLink(loginLink);
} else {
history.push(loginLink);
} }
history.push(loginLink);
} }
function renderLink(url, text, onClick) { function renderLink(url, text, onClick) {
@ -1070,18 +1080,28 @@ export function getTagColor(s) {
return "processing"; return "processing";
} }
export function getTags(tags) { export function getTags(tags, urlPrefix = null) {
const res = []; const res = [];
if (!tags) { if (!tags) {
return res; return res;
} }
tags.forEach((tag, i) => { tags.forEach((tag, i) => {
res.push( if (urlPrefix === null) {
<Tag color={getTagColor(tag)}> res.push(
{tag} <Tag color={getTagColor(tag)}>
</Tag> {tag}
); </Tag>
);
} else {
res.push(
<Link to={`/${urlPrefix}/${tag}`}>
<Tag color={getTagColor(tag)}>
{tag}
</Tag>
</Link>
);
}
}); });
return res; return res;
} }

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as SyncerBackend from "./backend/SyncerBackend"; import * as SyncerBackend from "./backend/SyncerBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class SyncerListPage extends BaseListPage { class SyncerListPage extends BaseListPage {
newSyncer() { newSyncer() {
@ -253,7 +253,7 @@ class SyncerListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={syncers} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={syncers} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Syncers")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Syncers")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -17,6 +17,7 @@ import * as SystemBackend from "./backend/SystemInfo";
import React from "react"; import React from "react";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import PrometheusInfoTable from "./table/PrometheusInfoTable";
class SystemInfo extends React.Component { class SystemInfo extends React.Component {
@ -25,6 +26,7 @@ class SystemInfo extends React.Component {
this.state = { this.state = {
systemInfo: {cpuUsage: [], memoryUsed: 0, memoryTotal: 0}, systemInfo: {cpuUsage: [], memoryUsed: 0, memoryTotal: 0},
versionInfo: {}, versionInfo: {},
prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
intervalId: null, intervalId: null,
loading: true, loading: true,
}; };
@ -45,6 +47,11 @@ class SystemInfo extends React.Component {
}).catch(error => { }).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`); Setting.showMessage("error", `System info failed to get: ${error}`);
}); });
SystemBackend.getPrometheusInfo().then(res => {
this.setState({
prometheusInfo: res.data,
});
});
}, 1000 * 2); }, 1000 * 2);
this.setState({intervalId: id}); this.setState({intervalId: id});
}).catch(error => { }).catch(error => {
@ -80,7 +87,10 @@ class SystemInfo extends React.Component {
<br /> <br /> <br /> <br />
<Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} /> <Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} />
</div>; </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}` : ""; 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"); let versionText = this.state.versionInfo?.version !== "" ? this.state.versionInfo?.version : i18next.t("system:Unknown version");
if (this.state.versionInfo?.commitOffset > 0) { if (this.state.versionInfo?.commitOffset > 0) {
@ -103,6 +113,16 @@ class SystemInfo extends React.Component {
{this.state.loading ? <Spin size="large" /> : memUi} {this.state.loading ? <Spin size="large" /> : memUi}
</Card> </Card>
</Col> </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> </Row>
<Divider /> <Divider />
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}> <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 * as TokenBackend from "./backend/TokenBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
class TokenListPage extends BaseListPage { class TokenListPage extends BaseListPage {
newToken() { newToken() {
@ -222,7 +222,7 @@ class TokenListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={tokens} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={tokens} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Tokens")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Tokens")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from "antd"; import {Button, Card, Col, Input, InputNumber, Result, Row, Select, Spin, Switch} from "antd";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -110,9 +110,9 @@ class UserEditPage extends React.Component {
} }
parseUserField(key, value) { parseUserField(key, value) {
// if ([].includes(key)) { if (["score", "karma", "ranking"].includes(key)) {
// value = Setting.myParseInt(value); value = Setting.myParseInt(value);
// } }
return value; return value;
} }
@ -360,6 +360,19 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "Address") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Address"), i18next.t("user:Address - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.address} onChange={e => {
this.updateUserField("address", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Affiliation") { } else if (accountItem.name === "Affiliation") {
return ( return (
(this.state.application === null || this.state.user === null) ? null : ( (this.state.application === null || this.state.user === null) ? null : (
@ -379,6 +392,32 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "ID card type") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:ID card type"), i18next.t("user:ID card type - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.idCardType} onChange={e => {
this.updateUserField("idCardType", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "ID card") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{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 => {
this.updateUserField("idCard", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Homepage") { } else if (accountItem.name === "Homepage") {
return ( return (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@ -431,6 +470,97 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "Language") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Language"), i18next.t("user:Language - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.language} onChange={e => {
this.updateUserField("language", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Gender") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Gender"), i18next.t("user:Gender - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.gender} onChange={e => {
this.updateUserField("gender", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Birthday") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Birthday"), i18next.t("user:Birthday - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.birthday} onChange={e => {
this.updateUserField("birthday", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Education") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Education"), i18next.t("user:Education - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.education} onChange={e => {
this.updateUserField("education", e.target.value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Score") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Score"), i18next.t("user:Score - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.user.score} onChange={value => {
this.updateUserField("score", value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Karma") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Karma"), i18next.t("user:Karma - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.user.karma} onChange={value => {
this.updateUserField("karma", value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Ranking") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Ranking"), i18next.t("user:Ranking - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.user.ranking} onChange={value => {
this.updateUserField("ranking", value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Signup application") { } else if (accountItem.name === "Signup application") {
return ( return (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >

View File

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

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