Compare commits

..

43 Commits

Author SHA1 Message Date
eb39e9e044 feat: add multi-factor authentication (MFA) feature (#1800)
* feat: add two-factor authentication interface and api

* merge

* feat: add Two-factor authentication accountItem and two-factor api in frontend

* feat: add basic 2fa setup UI

* rebase

* feat: finish the two-factor authentication

* rebase

* feat: support recover code

* chore: fix eslint error

* feat: support multiple sms account

* fix: client application login

* fix: lint

* Update authz.go

* Update mfa.go

* fix: support phone

* fix: i18n

* fix: i18n

* fix: support preferred mfa methods

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-05-05 21:23:59 +08:00
5b27f939b8 Improve add model initialization 2023-05-05 01:51:34 +08:00
69ee6a6f7e Move result page into entry page 2023-05-05 01:08:56 +08:00
bf6d5e529b Add "from" to Email provider 2023-05-04 23:41:37 +08:00
55fd31f575 Disable built-in/admin's unexpected change 2023-05-04 22:12:57 +08:00
05c063ac24 Set email's SkipUsernameCheck to true 2023-05-04 00:29:12 +08:00
38da63e73c Improve answer text 2023-05-02 23:33:09 +08:00
cb13d693e6 Add getTokenSize() 2023-05-02 10:04:11 +08:00
d699774179 Improve i18n.Translate() 2023-05-02 01:30:32 +08:00
84a7fdcd07 Handle message answer 2023-05-02 01:30:06 +08:00
2cd6f9df8e Add /api/get-message-answer API 2023-05-01 23:15:51 +08:00
eea2e1d271 Add ai package 2023-05-01 17:19:45 +08:00
48c5bd942c Fix chat UI 2023-05-01 16:23:48 +08:00
d01d63d82a Improve chat menu height 2023-05-01 14:11:17 +08:00
e4fd9cca92 Fix new chat button 2023-05-01 13:27:49 +08:00
8d531b8880 Fix getStateFromQueryParams() crash when provider name is non-latin 2023-05-01 10:32:08 +08:00
b1589e11eb Fix signin preview when there's no redirectUris 2023-05-01 10:31:21 +08:00
b32a772a77 Add jobNumber to dingtalk provider 2023-04-29 21:48:52 +08:00
7e4562efe1 Change org.defaultAvatar to 200 length 2023-04-29 08:33:04 +08:00
3a6ab4cfc6 Support mobile in DingTalk userinfo 2023-04-29 01:24:45 +08:00
fba4801a41 feat: make redirectUri token param follow OAuth2 standard (#1796)
* fix: rename token to access_token in implicit flow; change ? in the redirect uri to &

* fix typo
2023-04-28 23:54:48 +08:00
da21c92815 feat: support sub role synced update (#1794) 2023-04-28 22:14:37 +08:00
66c15578b1 feat: fix the order of Method and Name in System Info (#1797)
* fix: fixed the order of Method and Name in System Info

* fix: add i18n for System Info
2023-04-28 22:11:10 +08:00
f272be67ab Improve i18n 2023-04-28 18:43:41 +08:00
e4c36d407f feat: fix prometheus filter bugs (#1792)
* fix: fix prometheus

* fix: count latency with prefix api

* fix: latency should not be counted when startTime is nil
2023-04-26 22:18:48 +08:00
4c1915b014 fix: make query with like more precise (#1791) 2023-04-26 18:21:13 +08:00
6c2b172aae feat: fix function CheckAccountItemModifyRule (#1789)
* feat: fix function CheckAccountItemModifyRule

* fix: admin changes its own username

* fix: current user changes its own username

* Update user.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-04-26 16:21:58 +08:00
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
125 changed files with 7177 additions and 3846 deletions

View File

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

135
ai/ai.go Normal file
View File

@ -0,0 +1,135 @@
// 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 ai
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/sashabaranov/go-openai"
)
func queryAnswer(authToken string, question string, timeout int) (string, error) {
// fmt.Printf("Question: %s\n", question)
client := getProxyClientFromToken(authToken)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(2+timeout*2)*time.Second)
defer cancel()
resp, err := client.CreateChatCompletion(
ctx,
openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: question,
},
},
},
)
if err != nil {
return "", err
}
res := resp.Choices[0].Message.Content
res = strings.Trim(res, "\n")
// fmt.Printf("Answer: %s\n\n", res)
return res, nil
}
func QueryAnswerSafe(authToken string, question string) string {
var res string
var err error
for i := 0; i < 10; i++ {
res, err = queryAnswer(authToken, question, i)
if err != nil {
if i > 0 {
fmt.Printf("\tFailed (%d): %s\n", i+1, err.Error())
}
} else {
break
}
}
if err != nil {
panic(err)
}
return res
}
func QueryAnswerStream(authToken string, question string, writer io.Writer, builder *strings.Builder) error {
client := getProxyClientFromToken(authToken)
ctx := context.Background()
// https://platform.openai.com/tokenizer
// https://github.com/pkoukk/tiktoken-go#available-encodings
promptTokens, err := getTokenSize(openai.GPT3TextDavinci003, question)
if err != nil {
return err
}
// https://platform.openai.com/docs/models/gpt-3-5
maxTokens := 4097 - promptTokens
respStream, err := client.CreateCompletionStream(
ctx,
openai.CompletionRequest{
Model: openai.GPT3TextDavinci003,
Prompt: question,
MaxTokens: maxTokens,
Stream: true,
},
)
if err != nil {
return err
}
defer respStream.Close()
isLeadingReturn := true
for {
completion, streamErr := respStream.Recv()
if streamErr != nil {
if streamErr == io.EOF {
break
}
return streamErr
}
data := completion.Choices[0].Text
if isLeadingReturn && len(data) != 0 {
if strings.Count(data, "\n") == len(data) {
continue
} else {
isLeadingReturn = false
}
}
// Write the streamed data as Server-Sent Events
if _, err = fmt.Fprintf(writer, "data: %s\n\n", data); err != nil {
return err
}
// Append the response to the strings.Builder
builder.WriteString(data)
}
return nil
}

42
ai/ai_test.go Normal file
View File

@ -0,0 +1,42 @@
// 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.
//go:build !skipCi
// +build !skipCi
package ai
import (
"testing"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
"github.com/sashabaranov/go-openai"
)
func TestRun(t *testing.T) {
object.InitConfig()
proxy.InitHttpClient()
text, err := queryAnswer("", "hi", 5)
if err != nil {
panic(err)
}
println(text)
}
func TestToken(t *testing.T) {
println(getTokenSize(openai.GPT3TextDavinci003, ""))
}

28
ai/proxy.go Normal file
View File

@ -0,0 +1,28 @@
// 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 ai
import (
"github.com/casdoor/casdoor/proxy"
"github.com/sashabaranov/go-openai"
)
func getProxyClientFromToken(authToken string) *openai.Client {
config := openai.DefaultConfig(authToken)
config.HTTPClient = proxy.ProxyHttpClient
c := openai.NewClientWithConfig(config)
return c
}

28
ai/util.go Normal file
View File

@ -0,0 +1,28 @@
// 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 ai
import "github.com/pkoukk/tiktoken-go"
func getTokenSize(model string, prompt string) (int, error) {
tkm, err := tiktoken.EncodingForModel(model)
if err != nil {
return 0, err
}
token := tkm.Encode(prompt, nil, nil)
res := len(token)
return res, nil
}

View File

@ -90,6 +90,7 @@ p, *, *, GET, /api/userinfo, *, *
p, *, *, GET, /api/user, *, *
p, *, *, POST, /api/webhook, *, *
p, *, *, GET, /api/get-webhook-event, *, *
p, *, *, GET, /api/get-captcha-status, *, *
p, *, *, *, /api/login/oauth, *, *
p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-organization-applications, *, *
@ -120,6 +121,8 @@ p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *
p, *, *, GET, /api/get-prometheus-info, *, *
p, *, *, *, /api/metrics, *, *
`
sa := stringadapter.NewAdapter(ruleText)

View File

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

View File

@ -24,10 +24,10 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
@ -56,7 +56,7 @@ func tokenToResponse(token *object.Token) *Response {
}
// HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
userId := user.GetId()
allowed, err := object.CheckAccessPermission(userId, application)
@ -69,6 +69,12 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
return
}
if form.Password != "" && user.IsMfaEnabled() {
c.setMfaSessionData(&object.MfaSessionData{UserId: userId})
resp = &Response{Status: object.NextMfa, Data: user.GetPreferMfa(true)}
return
}
if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
@ -132,11 +138,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
// if user did not check auto signin
if resp.Status == "ok" && !form.AutoSignin {
timestamp := time.Now().Unix()
timestamp += 3600 * 24
c.SetSessionData(&SessionData{
ExpireTime: timestamp,
})
c.setExpireForSession()
}
if resp.Status == "ok" && user.Owner == object.CasdoorOrganization && application.Name == object.CasdoorApplication {
@ -221,21 +223,21 @@ func isProxyProviderType(providerType string) bool {
// @Param nonce query string false nonce
// @Param code_challenge_method query string false code_challenge_method
// @Param code_challenge query string false code_challenge
// @Param form body controllers.RequestForm true "Login information"
// @Param form body controllers.AuthForm true "Login information"
// @Success 200 {object} Response The Response object
// @router /login [post]
func (c *ApiController) Login() {
resp := &Response{}
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
var authForm form.AuthForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
if err != nil {
c.ResponseError(err.Error())
return
}
if form.Username != "" {
if form.Type == ResponseTypeLogin {
if authForm.Username != "" {
if authForm.Type == ResponseTypeLogin {
if c.GetSessionUsername() != "" {
c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername())
return
@ -245,25 +247,25 @@ func (c *ApiController) Login() {
var user *object.User
var msg string
if form.Password == "" {
if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
if authForm.Password == "" {
if user = object.GetUserByFields(authForm.Organization, authForm.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(authForm.Organization, authForm.Username)))
return
}
verificationCodeType := object.GetVerifyType(form.Username)
verificationCodeType := object.GetVerifyType(authForm.Username)
var checkDest string
if verificationCodeType == object.VerifyTypePhone {
form.CountryCode = user.GetCountryCode(form.CountryCode)
authForm.CountryCode = user.GetCountryCode(authForm.CountryCode)
var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), form.CountryCode))
if checkDest, ok = util.GetE164Number(authForm.Username, authForm.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), authForm.CountryCode))
return
}
}
// check result through Email or Phone
checkResult := object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage())
checkResult := object.CheckSigninCode(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
return
@ -272,18 +274,18 @@ func (c *ApiController) Login() {
// disable the verification code
object.DisableVerificationCode(checkDest)
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
if !application.EnablePassword {
c.ResponseError(c.T("auth:The login method: login with password is not enabled for the application"))
return
}
if object.CheckToEnableCaptcha(application) {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(form.CaptchaType, form.CaptchaToken, form.ClientSecret)
var enableCaptcha bool
if enableCaptcha = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); enableCaptcha {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
@ -295,41 +297,41 @@ func (c *ApiController) Login() {
}
}
password := form.Password
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage())
password := authForm.Password
user, msg = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
}
if msg != "" {
resp = &Response{Status: "error", Msg: msg}
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
}
} else if form.Provider != "" {
} else if authForm.Provider != "" {
var application *object.Application
if form.ClientId != "" {
application = object.GetApplicationByClientId(form.ClientId)
if authForm.ClientId != "" {
application = object.GetApplicationByClientId(authForm.ClientId)
} else {
application = object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", application.Organization))
provider := object.GetProvider(util.GetId("admin", form.Provider))
provider := object.GetProvider(util.GetId("admin", authForm.Provider))
providerItem := application.GetProviderItem(provider.Name)
if !providerItem.IsProviderVisible() {
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s is not enabled for the application"), provider.Name))
@ -339,7 +341,7 @@ func (c *ApiController) Login() {
userInfo := &idp.UserInfo{}
if provider.Category == "SAML" {
// SAML
userInfo.Id, err = object.ParseSamlResponse(form.SamlResponse, provider, c.Ctx.Request.Host)
userInfo.Id, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error())
return
@ -354,7 +356,7 @@ func (c *ApiController) Login() {
clientSecret = provider.ClientSecret2
}
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl)
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, authForm.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl)
if idProvider == nil {
c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type))
return
@ -362,13 +364,13 @@ func (c *ApiController) Login() {
setHttpClient(idProvider, provider.Type)
if form.State != conf.GetConfigString("authState") && form.State != application.Name {
c.ResponseError(fmt.Sprintf(c.T("auth:State expected: %s, but got: %s"), conf.GetConfigString("authState"), form.State))
if authForm.State != conf.GetConfigString("authState") && authForm.State != application.Name {
c.ResponseError(fmt.Sprintf(c.T("auth:State expected: %s, but got: %s"), conf.GetConfigString("authState"), authForm.State))
return
}
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
token, err := idProvider.GetToken(form.Code)
token, err := idProvider.GetToken(authForm.Code)
if err != nil {
c.ResponseError(err.Error())
return
@ -386,7 +388,7 @@ func (c *ApiController) Login() {
}
}
if form.Method == "signup" {
if authForm.Method == "signup" {
user := &object.User{}
if provider.Category == "SAML" {
user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id))
@ -401,7 +403,7 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
}
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
@ -456,6 +458,9 @@ func (c *ApiController) Login() {
Avatar: userInfo.AvatarUrl,
Address: []string{},
Email: userInfo.Email,
Phone: userInfo.Phone,
CountryCode: userInfo.CountryCode,
Region: userInfo.CountryCode,
Score: initScore,
IsAdmin: false,
IsGlobalAdmin: false,
@ -476,7 +481,7 @@ func (c *ApiController) Login() {
object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
object.LinkUserAccount(user, provider.Type, userInfo.Id)
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
@ -492,7 +497,7 @@ func (c *ApiController) Login() {
resp = &Response{Status: "error", Msg: fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id))}
}
// resp = &Response{Status: "ok", Msg: "", Data: res}
} else { // form.Method != "signup"
} else { // authForm.Method != "signup"
userId := c.GetSessionUsername()
if userId == "" {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id)), userInfo)
@ -517,24 +522,56 @@ func (c *ApiController) Login() {
resp = &Response{Status: "error", Msg: "Failed to link user account", Data: isLinked}
}
}
} else if c.getMfaSessionData() != nil {
mfaSession := c.getMfaSessionData()
user := object.GetUser(mfaSession.UserId)
if authForm.Passcode != "" {
MfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferMfa(false))
err = MfaUtil.Verify(authForm.Passcode)
if err != nil {
c.ResponseError(err.Error())
return
}
}
if authForm.RecoveryCode != "" {
err = object.RecoverTfs(user, authForm.RecoveryCode)
if err != nil {
c.ResponseError(err.Error())
return
}
}
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else {
if c.GetSessionUsername() != "" {
// user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), form.Application))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &form)
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else {
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), form = %s"), util.StructToJson(form)))
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), form = %s"), util.StructToJson(authForm)))
return
}
}
@ -610,3 +647,21 @@ func (c *ApiController) GetWebhookEventType() {
wechatScanType = ""
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,44 @@ type SessionData struct {
}
func (c *ApiController) IsGlobalAdmin() bool {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {
// e.g., "app/app-casnode"
return true
}
isGlobalAdmin, _ := c.isGlobalAdmin()
user := object.GetUser(username)
return isGlobalAdmin
}
func (c *ApiController) IsAdmin() bool {
isGlobalAdmin, user := c.isGlobalAdmin()
if user == nil {
return false
}
return user.Owner == "built-in" || user.IsGlobalAdmin
return isGlobalAdmin || user.IsAdmin
}
func (c *ApiController) isGlobalAdmin() (bool, *object.User) {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {
// e.g., "app/app-casnode"
return true, nil
}
user := c.getCurrentUser()
if user == nil {
return false, nil
}
return user.Owner == "built-in" || user.IsGlobalAdmin, user
}
func (c *ApiController) getCurrentUser() *object.User {
var user *object.User
userId := c.GetSessionUsername()
if userId == "" {
user = nil
} else {
user = object.GetUser(userId)
}
return user
}
// GetSessionUsername ...
@ -142,6 +168,30 @@ func (c *ApiController) SetSessionData(s *SessionData) {
c.SetSession("SessionData", util.StructToJson(s))
}
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) {
c.SetSession(object.MfaSessionUserId, data.UserId)
}
func (c *ApiController) getMfaSessionData() *object.MfaSessionData {
userId := c.GetSession(object.MfaSessionUserId)
if userId == nil {
return nil
}
data := &object.MfaSessionData{
UserId: userId.(string),
}
return data
}
func (c *ApiController) setExpireForSession() {
timestamp := time.Now().Unix()
timestamp += 3600 * 24
c.SetSessionData(&SessionData{
ExpireTime: timestamp,
})
}
func wrapActionResponse(affected bool) *Response {
if affected {
return &Response{Status: "ok", Msg: "", Data: "Affected"}
@ -157,3 +207,14 @@ func wrapErrorResponse(err error) *Response {
return &Response{Status: "error", Msg: err.Error()}
}
}
func (c *ApiController) Finish() {
if strings.HasPrefix(c.Ctx.Input.URL(), "/api") {
startTime := c.Ctx.Input.GetData("startTime")
if startTime != nil {
latency := time.Since(startTime.(time.Time)).Milliseconds()
object.ApiLatency.WithLabelValues(c.Ctx.Input.URL(), c.Ctx.Input.Method()).Observe(float64(latency))
}
}
c.Controller.Finish()
}

View File

@ -16,8 +16,11 @@ package controllers
import (
"encoding/json"
"fmt"
"strings"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/ai"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@ -71,6 +74,97 @@ func (c *ApiController) GetMessage() {
c.ServeJSON()
}
func (c *ApiController) ResponseErrorStream(errorText string) {
event := fmt.Sprintf("event: myerror\ndata: %s\n\n", errorText)
_, err := c.Ctx.ResponseWriter.Write([]byte(event))
if err != nil {
panic(err)
}
}
// GetMessageAnswer
// @Title GetMessageAnswer
// @Tag Message API
// @Description get message answer
// @Param id query string true "The id ( owner/name ) of the message"
// @Success 200 {object} object.Message The Response object
// @router /get-message-answer [get]
func (c *ApiController) GetMessageAnswer() {
id := c.Input().Get("id")
c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream")
c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache")
c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive")
message := object.GetMessage(id)
if message == nil {
c.ResponseErrorStream(fmt.Sprintf(c.T("chat:The message: %s is not found"), id))
return
}
if message.Author != "AI" || message.ReplyTo == "" || message.Text != "" {
c.ResponseErrorStream(c.T("chat:The message is invalid"))
return
}
chatId := util.GetId(message.Owner, message.Chat)
chat := object.GetChat(chatId)
if chat == nil {
c.ResponseErrorStream(fmt.Sprintf(c.T("chat:The chat: %s is not found"), chatId))
return
}
if chat.Type != "AI" {
c.ResponseErrorStream(c.T("chat:The chat type must be \"AI\""))
return
}
questionMessage := object.GetMessage(message.ReplyTo)
if questionMessage == nil {
c.ResponseErrorStream(fmt.Sprintf(c.T("chat:The message: %s is not found"), id))
return
}
providerId := util.GetId(chat.Owner, chat.User2)
provider := object.GetProvider(providerId)
if provider == nil {
c.ResponseErrorStream(fmt.Sprintf(c.T("chat:The provider: %s is not found"), providerId))
return
}
if provider.Category != "AI" || provider.ClientSecret == "" {
c.ResponseErrorStream(fmt.Sprintf(c.T("chat:The provider: %s is invalid"), providerId))
return
}
c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream")
c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache")
c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive")
authToken := provider.ClientSecret
question := questionMessage.Text
var stringBuilder strings.Builder
err := ai.QueryAnswerStream(authToken, question, c.Ctx.ResponseWriter, &stringBuilder)
if err != nil {
c.ResponseErrorStream(err.Error())
return
}
event := fmt.Sprintf("event: end\ndata: %s\n\n", "end")
_, err = c.Ctx.ResponseWriter.Write([]byte(event))
if err != nil {
panic(err)
}
answer := stringBuilder.String()
fmt.Printf("Question: [%s]\n", questionMessage.Text)
fmt.Printf("Answer: [%s]\n", answer)
message.Text = answer
object.UpdateMessage(message.GetId(), message)
}
// UpdateMessage
// @Title UpdateMessage
// @Tag Message API
@ -108,7 +202,26 @@ func (c *ApiController) AddMessage() {
return
}
c.Data["json"] = wrapActionResponse(object.AddMessage(&message))
affected := object.AddMessage(&message)
if affected {
chatId := util.GetId(message.Owner, message.Chat)
chat := object.GetChat(chatId)
if chat != nil && chat.Type == "AI" {
answerMessage := &object.Message{
Owner: message.Owner,
Name: fmt.Sprintf("message_%s", util.GetRandomName()),
CreatedTime: util.GetCurrentTimeEx(message.CreatedTime),
Organization: message.Organization,
Chat: message.Chat,
ReplyTo: message.GetId(),
Author: "AI",
Text: "",
}
object.AddMessage(answerMessage)
}
}
c.Data["json"] = wrapActionResponse(affected)
c.ServeJSON()
}

190
controllers/mfa.go Normal file
View File

@ -0,0 +1,190 @@
// 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 (
"net/http"
"github.com/beego/beego"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// MfaSetupInitiate
// @Title MfaSetupInitiate
// @Tag MFA API
// @Description setup MFA
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param type form string true "MFA auth type"
// @Success 200 {object} The Response object
// @router /mfa/setup/initiate [post]
func (c *ApiController) MfaSetupInitiate() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
authType := c.Ctx.Request.Form.Get("type")
userId := util.GetId(owner, name)
if len(userId) == 0 {
c.ResponseError(http.StatusText(http.StatusBadRequest))
return
}
MfaUtil := object.GetMfaUtil(authType, nil)
if MfaUtil == nil {
c.ResponseError("Invalid auth type")
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError("User doesn't exist")
return
}
issuer := beego.AppConfig.String("appname")
accountName := user.GetId()
mfaProps, err := MfaUtil.Initiate(c.Ctx, issuer, accountName)
if err != nil {
c.ResponseError(err.Error())
return
}
resp := mfaProps
c.ResponseOk(resp)
}
// MfaSetupVerify
// @Title MfaSetupVerify
// @Tag MFA API
// @Description setup verify totp
// @param secret form string true "MFA secret"
// @param passcode form string true "MFA passcode"
// @Success 200 {object} Response object
// @router /mfa/setup/verify [post]
func (c *ApiController) MfaSetupVerify() {
authType := c.Ctx.Request.Form.Get("type")
passcode := c.Ctx.Request.Form.Get("passcode")
if authType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode")
return
}
MfaUtil := object.GetMfaUtil(authType, nil)
err := MfaUtil.SetupVerify(c.Ctx, passcode)
if err != nil {
c.ResponseError(err.Error())
} else {
c.ResponseOk(http.StatusText(http.StatusOK))
}
}
// MfaSetupEnable
// @Title MfaSetupEnable
// @Tag MFA API
// @Description enable totp
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param type form string true "MFA auth type"
// @Success 200 {object} Response object
// @router /mfa/setup/enable [post]
func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
authType := c.Ctx.Request.Form.Get("type")
user := object.GetUser(util.GetId(owner, name))
if user == nil {
c.ResponseError("User doesn't exist")
return
}
twoFactor := object.GetMfaUtil(authType, nil)
err := twoFactor.Enable(c.Ctx, user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(http.StatusText(http.StatusOK))
}
// DeleteMfa
// @Title DeleteMfa
// @Tag MFA API
// @Description: Delete MFA
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param id form string true "id of user's MFA props"
// @Success 200 {object} Response object
// @router /delete-mfa/ [post]
func (c *ApiController) DeleteMfa() {
id := c.Ctx.Request.Form.Get("id")
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
userId := util.GetId(owner, name)
user := object.GetUser(userId)
if user == nil {
c.ResponseError("User doesn't exist")
return
}
mfaProps := user.MultiFactorAuths[:0]
i := 0
for _, mfaProp := range mfaProps {
if mfaProp.Id != id {
mfaProps[i] = mfaProp
i++
}
}
user.MultiFactorAuths = mfaProps
object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
c.ResponseOk(user.MultiFactorAuths)
}
// SetPreferredMfa
// @Title SetPreferredMfa
// @Tag MFA API
// @Description: Set specific Mfa Preferred
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param id form string true "id of user's MFA props"
// @Success 200 {object} Response object
// @router /set-preferred-mfa [post]
func (c *ApiController) SetPreferredMfa() {
id := c.Ctx.Request.Form.Get("id")
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
userId := util.GetId(owner, name)
user := object.GetUser(userId)
if user == nil {
c.ResponseError("User doesn't exist")
return
}
mfaProps := user.MultiFactorAuths
for i, mfaProp := range user.MultiFactorAuths {
if mfaProp.Id == id {
mfaProps[i].IsPreferred = true
} else {
mfaProps[i].IsPreferred = false
}
}
object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
c.ResponseOk(user.MultiFactorAuths)
}

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()
}
// GetOAuthCode
// @Title GetOAuthCode
// @Tag Token API
// @Description get OAuth code
// @Param user_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
// @Title GetOAuthToken
// @Tag Token API

View File

@ -158,11 +158,18 @@ func (c *ApiController) UpdateUser() {
return
}
if oldUser.Owner == "built-in" && oldUser.Name == "admin" && (user.Owner != "built-in" || user.Name != "admin") {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg)
return
}
if pass, err := checkPermissionForUpdateUser(oldUser, &user, c); !pass {
isAdmin := c.IsAdmin()
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, c.GetAcceptLanguage()); !pass {
c.ResponseError(err)
return
}
@ -172,9 +179,7 @@ func (c *ApiController) UpdateUser() {
columns = strings.Split(columnsStr, ",")
}
isGlobalAdmin := c.IsGlobalAdmin()
affected := object.UpdateUser(id, &user, columns, isGlobalAdmin)
affected := object.UpdateUser(id, &user, columns, isAdmin)
if affected {
object.UpdateUserToOriginalDatabase(&user)
}
@ -229,6 +234,11 @@ func (c *ApiController) DeleteUser() {
return
}
if user.Owner == "built-in" && user.Name == "admin" {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
c.Data["json"] = wrapActionResponse(object.DeleteUser(&user))
c.ServeJSON()
}
@ -286,6 +296,11 @@ func (c *ApiController) SetPassword() {
newPassword := c.Ctx.Request.Form.Get("newPassword")
code := c.Ctx.Request.Form.Get("code")
//if userOwner == "built-in" && userName == "admin" {
// c.ResponseError(c.T("auth:Unauthorized operation"))
// return
//}
if strings.Contains(newPassword, " ") {
c.ResponseError(c.T("user:New password cannot contain blank space."))
return

View File

@ -1,138 +0,0 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casdoor/casdoor/object"
)
func checkPermissionForUpdateUser(oldUser, newUser *object.User, c *ApiController) (bool, string) {
organization := object.GetOrganizationByUser(oldUser)
var itemsChanged []*object.AccountItem
if oldUser.Owner != newUser.Owner {
item := object.GetAccountItemByName("Organization", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Name != newUser.Name {
item := object.GetAccountItemByName("Name", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Id != newUser.Id {
item := object.GetAccountItemByName("ID", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.DisplayName != newUser.DisplayName {
item := object.GetAccountItemByName("Display name", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Avatar != newUser.Avatar {
item := object.GetAccountItemByName("Avatar", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Type != newUser.Type {
item := object.GetAccountItemByName("User type", organization)
itemsChanged = append(itemsChanged, item)
}
// The password is *** when not modified
if oldUser.Password != newUser.Password && newUser.Password != "***" {
item := object.GetAccountItemByName("Password", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Email != newUser.Email {
item := object.GetAccountItemByName("Email", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Phone != newUser.Phone {
item := object.GetAccountItemByName("Phone", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.CountryCode != newUser.CountryCode {
item := object.GetAccountItemByName("Country code", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Region != newUser.Region {
item := object.GetAccountItemByName("Country/Region", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Location != newUser.Location {
item := object.GetAccountItemByName("Location", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Affiliation != newUser.Affiliation {
item := object.GetAccountItemByName("Affiliation", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Title != newUser.Title {
item := object.GetAccountItemByName("Title", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Homepage != newUser.Homepage {
item := object.GetAccountItemByName("Homepage", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Bio != newUser.Bio {
item := object.GetAccountItemByName("Bio", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Tag != newUser.Tag {
item := object.GetAccountItemByName("Tag", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.SignupApplication != newUser.SignupApplication {
item := object.GetAccountItemByName("Signup application", organization)
itemsChanged = append(itemsChanged, item)
}
oldUserPropertiesJson, _ := json.Marshal(oldUser.Properties)
newUserPropertiesJson, _ := json.Marshal(newUser.Properties)
if string(oldUserPropertiesJson) != string(newUserPropertiesJson) {
item := object.GetAccountItemByName("Properties", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsAdmin != newUser.IsAdmin {
item := object.GetAccountItemByName("Is admin", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsGlobalAdmin != newUser.IsGlobalAdmin {
item := object.GetAccountItemByName("Is global admin", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsForbidden != newUser.IsForbidden {
item := object.GetAccountItemByName("Is forbidden", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsDeleted != newUser.IsDeleted {
item := object.GetAccountItemByName("Is deleted", organization)
itemsChanged = append(itemsChanged, item)
}
currentUser := c.getCurrentUser()
if currentUser == nil && c.IsGlobalAdmin() {
currentUser = &object.User{
IsGlobalAdmin: true,
}
}
for i := range itemsChanged {
if pass, err := object.CheckAccountItemModifyRule(itemsChanged[i], currentUser, c.GetAcceptLanguage()); !pass {
return pass, err
}
}
return true, ""
}

View File

@ -128,7 +128,7 @@ func (c *ApiController) GetProviderFromContext(category string) (*object.Provide
if providerName != "" {
provider := object.GetProvider(util.GetId("admin", providerName))
if provider == nil {
c.ResponseError(c.T("util:The provider: %s is not found"), providerName)
c.ResponseError(fmt.Sprintf(c.T("util:The provider: %s is not found"), providerName))
return nil, nil, false
}
return provider, nil, true

View File

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

View File

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

57
form/auth.go Normal file
View File

@ -0,0 +1,57 @@
// 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"`
MfaType string `json:"mfaType"`
Passcode string `json:"passcode"`
RecoveryCode string `json:"recoveryCode"`
}

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 ""
}

7
go.mod
View File

@ -17,6 +17,7 @@ require (
github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0
github.com/dlclark/regexp2 v1.9.0 // indirect
github.com/fogleman/gg v1.3.0
github.com/forestmgy/ldapserver v1.1.0
github.com/go-git/go-git/v5 v5.6.0
@ -36,15 +37,19 @@ require (
github.com/markbates/goth v1.75.2
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5
github.com/pkoukk/tiktoken-go v0.1.1
github.com/prometheus/client_golang v1.7.0
github.com/prometheus/client_model v0.2.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0
github.com/russellhaering/goxmldsig v1.1.1
github.com/sashabaranov/go-openai v1.9.1
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.1
github.com/stretchr/testify v1.8.2
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
github.com/tklauser/go-sysconf v0.3.10 // indirect

10
go.sum
View File

@ -161,6 +161,9 @@ github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI=
github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
@ -476,6 +479,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -520,6 +525,8 @@ github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5se
github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sashabaranov/go-openai v1.9.1 h1:3N52HkJKo9Zlo/oe1AVv5ZkCOny0ra58/ACvAxkN3MM=
github.com/sashabaranov/go-openai v1.9.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
@ -571,8 +578,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Service %s und %s stimmen nicht überein"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein",
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
@ -52,6 +60,7 @@
"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",
"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",
"unsupported password type: %s": "Nicht unterstützter Passworttyp: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "Nicht in der Lage, die E-Mail-Änderungsregel zu erhalten.",
"Unable to get the phone modify rule.": "Nicht in der Lage, die Telefon-Änderungsregel zu erhalten.",
"Unknown type": "Unbekannter Typ",
"Wrong parameter": "Falscher Parameter",
"Wrong verification code!": "Falscher Bestätigungscode!",
"You should verify your code in %d min!": "Du solltest deinen Code in %d Minuten verifizieren!",
"the user does not exist, please sign up first": "Der Benutzer existiert nicht, bitte zuerst anmelden"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
@ -52,6 +60,7 @@
"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",
"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",
"unsupported password type: %s": "unsupported password type: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong parameter": "Wrong parameter",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Los servicios %s y %s no coinciden"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Afiliación no puede estar en blanco",
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
@ -52,6 +60,7 @@
"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",
"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",
"unsupported password type: %s": "Tipo de contraseña no compatible: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "No se puede obtener la regla de modificación de correo electrónico.",
"Unable to get the phone modify rule.": "No se pudo obtener la regla de modificación del teléfono.",
"Unknown type": "Tipo desconocido",
"Wrong parameter": "Parámetro incorrecto",
"Wrong verification code!": "¡Código de verificación incorrecto!",
"You should verify your code in %d min!": "¡Deberías verificar tu código en %d minutos!",
"the user does not exist, please sign up first": "El usuario no existe, por favor regístrese primero"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Les services %s et %s ne correspondent pas"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation ne peut pas être vide",
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
@ -52,6 +60,7 @@
"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",
"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",
"unsupported password type: %s": "Type de mot de passe non pris en charge : %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "Incapable d'obtenir la règle de modification de courriel.",
"Unable to get the phone modify rule.": "Impossible d'obtenir la règle de modification de téléphone.",
"Unknown type": "Type inconnu",
"Wrong parameter": "Mauvais paramètre",
"Wrong verification code!": "Mauvais code de vérification !",
"You should verify your code in %d min!": "Vous devriez vérifier votre code en %d min !",
"the user does not exist, please sign up first": "L'utilisateur n'existe pas, veuillez vous inscrire d'abord"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Layanan %s dan %s tidak cocok"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Keterkaitan tidak boleh kosong",
"DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong",
@ -52,6 +60,7 @@
"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",
"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",
"unsupported password type: %s": "jenis sandi tidak didukung: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "Tidak dapat memperoleh aturan modifikasi email.",
"Unable to get the phone modify rule.": "Tidak dapat memodifikasi aturan telepon.",
"Unknown type": "Tipe tidak diketahui",
"Wrong parameter": "Parameter yang salah",
"Wrong verification code!": "Kode verifikasi salah!",
"You should verify your code in %d min!": "Anda harus memverifikasi kode Anda dalam %d menit!",
"the user does not exist, please sign up first": "Pengguna tidak ada, silakan daftar terlebih dahulu"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "サービス%sと%sは一致しません"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "所属は空白にできません",
"DisplayName cannot be blank": "表示名は空白にできません",
@ -52,6 +60,7 @@
"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 分間待ってから再度お試しください",
"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回の試行機会があります",
"unsupported password type: %s": "サポートされていないパスワードタイプ:%s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "電子メール変更規則を取得できません。",
"Unable to get the phone modify rule.": "電話の変更ルールを取得できません。",
"Unknown type": "不明なタイプ",
"Wrong parameter": "誤ったパラメータ",
"Wrong verification code!": "誤った検証コードです!",
"You should verify your code in %d min!": "あなたは%d分であなたのコードを確認する必要があります",
"the user does not exist, please sign up first": "ユーザーは存在しません。まず登録してください"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "서비스 %s와 %s는 일치하지 않습니다"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "소속은 비워 둘 수 없습니다",
"DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다",
@ -52,6 +60,7 @@
"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분 동안 기다리신 후 다시 시도해주세요",
"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번의 기회가 남아 있습니다",
"unsupported password type: %s": "지원되지 않는 암호 유형: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "이메일 수정 규칙을 가져올 수 없습니다.",
"Unable to get the phone modify rule.": "전화 수정 규칙을 가져올 수 없습니다.",
"Unknown type": "알 수 없는 유형",
"Wrong parameter": "잘못된 매개 변수입니다",
"Wrong verification code!": "잘못된 인증 코드입니다!",
"You should verify your code in %d min!": "당신은 %d분 안에 코드를 검증해야 합니다!",
"the user does not exist, please sign up first": "사용자가 존재하지 않습니다. 먼저 회원 가입 해주세요"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Сервисы %s и %s не совпадают"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Принадлежность не может быть пустым значением",
"DisplayName cannot be blank": "Имя отображения не может быть пустым",
@ -52,6 +60,7 @@
"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 минут и попробуйте снова",
"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 попыток",
"unsupported password type: %s": "неподдерживаемый тип пароля: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.",
"Unable to get the phone modify rule.": "Невозможно получить правило изменения телефона.",
"Unknown type": "Неизвестный тип",
"Wrong parameter": "Неправильный параметр",
"Wrong verification code!": "Неправильный код подтверждения!",
"You should verify your code in %d min!": "Вы должны проверить свой код через %d минут!",
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Dịch sang tiếng Việt: Dịch vụ %s và %s không khớp"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Tình trạng liên kết không thể để trống",
"DisplayName cannot be blank": "Tên hiển thị không thể để trống",
@ -52,6 +60,7 @@
"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",
"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",
"unsupported password type: %s": "Loại mật khẩu không được hỗ trợ: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "Không thể lấy quy tắc sửa đổi email.",
"Unable to get the phone modify rule.": "Không thể thay đổi quy tắc trên điện thoại.",
"Unknown type": "Loại không xác định",
"Wrong parameter": "Tham số không đúng",
"Wrong verification code!": "Mã xác thực sai!",
"You should verify your code in %d min!": "Bạn nên kiểm tra mã của mình trong %d phút!",
"the user does not exist, please sign up first": "Người dùng không tồn tại, vui lòng đăng ký trước"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "服务%s与%s不匹配"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "工作单位不可为空",
"DisplayName cannot be blank": "显示名称不可为空",
@ -52,6 +60,7 @@
"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 分后重试",
"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 次尝试的机会",
"unsupported password type: %s": "不支持的密码类型: %s"
},
@ -129,7 +138,6 @@
"Unable to get the email modify rule.": "无法获取邮箱修改规则",
"Unable to get the phone modify rule.": "无法获取手机号修改规则",
"Unknown type": "未知类型",
"Wrong parameter": "参数错误",
"Wrong verification code!": "验证码错误!",
"You should verify your code in %d min!": "请在 %d 分钟内输入正确验证码",
"the user does not exist, please sign up first": "用户不存在,请先注册"

View File

@ -74,13 +74,12 @@ func applyData(data1 *I18nData, data2 *I18nData) {
}
func Translate(lang string, error string) string {
parts := strings.SplitN(error, ":", 2)
if !strings.Contains(error, ":") || len(parts) != 2 {
tokens := strings.SplitN(error, ":", 2)
if !strings.Contains(error, ":") || len(tokens) != 2 {
return "Translate Error: " + error
}
if langMap[lang] != nil {
return langMap[lang][parts[0]][parts[1]]
} else {
if langMap[lang] == nil {
file, _ := f.ReadFile("locales/" + lang + "/data.json")
data := I18nData{}
err := util.JsonToStruct(string(file), &data)
@ -88,6 +87,11 @@ func Translate(lang string, error string) string {
panic(err)
}
langMap[lang] = data
return langMap[lang][parts[0]][parts[1]]
}
res := langMap[lang][tokens[0]][tokens[1]]
if res == "" {
res = tokens[1]
}
return res
}

View File

@ -23,6 +23,7 @@ import (
"strings"
"time"
"github.com/casdoor/casdoor/util"
"golang.org/x/oauth2"
)
@ -125,8 +126,8 @@ type DingTalkUserResponse struct {
UnionId string `json:"unionId"`
AvatarUrl string `json:"avatarUrl"`
Email string `json:"email"`
Errmsg string `json:"message"`
Errcode string `json:"code"`
Mobile string `json:"mobile"`
StateCode string `json:"stateCode"`
}
// GetUserInfo Use access_token to get UserInfo
@ -156,8 +157,9 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
return nil, err
}
if dtUserInfo.Errmsg != "" {
return nil, fmt.Errorf("userIdResp.Errcode = %s, userIdResp.Errmsg = %s", dtUserInfo.Errcode, dtUserInfo.Errmsg)
countryCode, err := util.GetCountryCode(dtUserInfo.StateCode, dtUserInfo.Mobile)
if err != nil {
return nil, err
}
userInfo := UserInfo{
@ -166,6 +168,8 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
DisplayName: dtUserInfo.Nick,
UnionId: dtUserInfo.UnionId,
Email: dtUserInfo.Email,
Phone: dtUserInfo.Mobile,
CountryCode: countryCode,
AvatarUrl: dtUserInfo.AvatarUrl,
}
@ -175,9 +179,15 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
return nil, err
}
corpEmail, err := idp.getUserCorpEmail(userId, corpAccessToken)
if err == nil && corpEmail != "" {
userInfo.Email = corpEmail
corpEmail, jobNumber, err := idp.getUserCorpEmail(userId, corpAccessToken)
if err == nil {
if corpEmail != "" {
userInfo.Email = corpEmail
}
if jobNumber != "" {
userInfo.Username = jobNumber
}
}
return &userInfo, nil
@ -247,33 +257,34 @@ func (idp *DingTalkIdProvider) getUserId(unionId string, accessToken string) (st
return "", err
}
if data.ErrCode == 60121 {
return "", fmt.Errorf("the user is not found in the organization where clientId and clientSecret belong")
return "", fmt.Errorf("该应用只允许本企业内部用户登录,您不属于该企业,无法登录")
} else if data.ErrCode != 0 {
return "", fmt.Errorf(data.ErrMessage)
}
return data.Result.UserId, nil
}
func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken string) (string, error) {
func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken string) (string, string, error) {
body := make(map[string]string)
body["userid"] = userId
respBytes, err := idp.postWithBody(body, "https://oapi.dingtalk.com/topapi/v2/user/get?access_token="+accessToken)
if err != nil {
return "", err
return "", "", err
}
var data struct {
ErrMessage string `json:"errmsg"`
Result struct {
Email string `json:"email"`
Email string `json:"email"`
JobNumber string `json:"job_number"`
} `json:"result"`
}
err = json.Unmarshal(respBytes, &data)
if err != nil {
return "", err
return "", "", err
}
if data.ErrMessage != "ok" {
return "", fmt.Errorf(data.ErrMessage)
return "", "", fmt.Errorf(data.ErrMessage)
}
return data.Result.Email, nil
return data.Result.Email, data.Result.JobNumber, nil
}

View File

@ -27,6 +27,8 @@ type UserInfo struct {
DisplayName string
UnionId string
Email string
Phone string
CountryCode string
AvatarUrl string
}

View File

@ -60,6 +60,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AuthzFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
beego.BConfig.WebConfig.Session.SessionOn = true
beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id"
@ -82,6 +83,7 @@ func main() {
logs.SetLogFuncCall(false)
go ldap.StartLdapServer()
go object.ClearThroughputPerSecond()
beego.Run(fmt.Sprintf(":%v", port))
}

View File

@ -121,6 +121,13 @@ func UpdateChat(id string, chat *Chat) bool {
}
func AddChat(chat *Chat) bool {
if chat.Type == "AI" && chat.User2 == "" {
provider := getDefaultAiProvider()
if provider != nil {
chat.User2 = provider.Name
}
}
affected, err := adapter.Engine.Insert(chat)
if err != nil {
panic(err)
@ -135,6 +142,10 @@ func DeleteChat(chat *Chat) bool {
panic(err)
}
if affected != 0 {
return DeleteChatMessages(chat.Name)
}
return affected != 0
}

View File

@ -22,6 +22,7 @@ import (
"unicode"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
@ -42,86 +43,86 @@ func init() {
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, firstName string, lastName string, email string, phone string, countryCode string, affiliation string, lang string) string {
func CheckUserSignup(application *Application, organization *Organization, form *form.AuthForm, lang string) string {
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
}
if application.IsSignupItemVisible("Username") {
if len(username) <= 1 {
if len(form.Username) <= 1 {
return i18n.Translate(lang, "check:Username must have at least 2 characters")
}
if unicode.IsDigit(rune(username[0])) {
if unicode.IsDigit(rune(form.Username[0])) {
return i18n.Translate(lang, "check:Username cannot start with a digit")
}
if util.IsEmailValid(username) {
if util.IsEmailValid(form.Username) {
return i18n.Translate(lang, "check:Username cannot be an email address")
}
if reWhiteSpace.MatchString(username) {
if reWhiteSpace.MatchString(form.Username) {
return i18n.Translate(lang, "check:Username cannot contain white spaces")
}
if msg := CheckUsername(username, lang); msg != "" {
if msg := CheckUsername(form.Username, lang); msg != "" {
return msg
}
if HasUserByField(organization.Name, "name", username) {
if HasUserByField(organization.Name, "name", form.Username) {
return i18n.Translate(lang, "check:Username already exists")
}
if HasUserByField(organization.Name, "email", email) {
if HasUserByField(organization.Name, "email", form.Email) {
return i18n.Translate(lang, "check:Email already exists")
}
if HasUserByField(organization.Name, "phone", phone) {
if HasUserByField(organization.Name, "phone", form.Phone) {
return i18n.Translate(lang, "check:Phone already exists")
}
}
if len(password) <= 5 {
if len(form.Password) <= 5 {
return i18n.Translate(lang, "check:Password must have at least 6 characters")
}
if application.IsSignupItemVisible("Email") {
if email == "" {
if form.Email == "" {
if application.IsSignupItemRequired("Email") {
return i18n.Translate(lang, "check:Email cannot be empty")
}
} else {
if HasUserByField(organization.Name, "email", email) {
if HasUserByField(organization.Name, "email", form.Email) {
return i18n.Translate(lang, "check:Email already exists")
} else if !util.IsEmailValid(email) {
} else if !util.IsEmailValid(form.Email) {
return i18n.Translate(lang, "check:Email is invalid")
}
}
}
if application.IsSignupItemVisible("Phone") {
if phone == "" {
if form.Phone == "" {
if application.IsSignupItemRequired("Phone") {
return i18n.Translate(lang, "check:Phone cannot be empty")
}
} else {
if HasUserByField(organization.Name, "phone", phone) {
if HasUserByField(organization.Name, "phone", form.Phone) {
return i18n.Translate(lang, "check:Phone already exists")
} else if !util.IsPhoneAllowInRegin(countryCode, organization.CountryCodes) {
} else if !util.IsPhoneAllowInRegin(form.CountryCode, organization.CountryCodes) {
return i18n.Translate(lang, "check:Your region is not allow to signup by phone")
} else if !util.IsPhoneValid(phone, countryCode) {
} else if !util.IsPhoneValid(form.Phone, form.CountryCode) {
return i18n.Translate(lang, "check:Phone number is invalid")
}
}
}
if application.IsSignupItemVisible("Display name") {
if application.GetSignupItemRule("Display name") == "First, last" && (firstName != "" || lastName != "") {
if firstName == "" {
if application.GetSignupItemRule("Display name") == "First, last" && (form.FirstName != "" || form.LastName != "") {
if form.FirstName == "" {
return i18n.Translate(lang, "check:FirstName cannot be blank")
} else if lastName == "" {
} else if form.LastName == "" {
return i18n.Translate(lang, "check:LastName cannot be blank")
}
} else {
if displayName == "" {
if form.Name == "" {
return i18n.Translate(lang, "check:DisplayName cannot be blank")
} else if application.GetSignupItemRule("Display name") == "Real name" {
if !isValidRealName(displayName) {
if !isValidRealName(form.Name) {
return i18n.Translate(lang, "check:DisplayName is not valid real name")
}
}
@ -129,7 +130,7 @@ func CheckUserSignup(application *Application, organization *Organization, usern
}
if application.IsSignupItemVisible("Affiliation") {
if affiliation == "" {
if form.Affiliation == "" {
return i18n.Translate(lang, "check:Affiliation cannot be blank")
}
}
@ -157,10 +158,16 @@ func checkSigninErrorTimes(user *User, lang string) string {
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
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
if !enableCaptcha {
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
}
}
organization := GetOrganizationByUser(user)
@ -182,7 +189,7 @@ func CheckPassword(user *User, password string, lang string) string {
return ""
}
return recordSigninErrorInfo(user, lang)
return recordSigninErrorInfo(user, lang, enableCaptcha)
} else {
return fmt.Sprintf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
}
@ -231,7 +238,11 @@ func checkLdapUserPassword(user *User, password string, lang string) string {
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)
if user == nil || user.IsDeleted == true {
return nil, fmt.Sprintf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username))
@ -250,7 +261,7 @@ func CheckUserPassword(organization string, username string, password string, la
return nil, msg
}
} else {
if msg := CheckPassword(user, password, lang); msg != "" {
if msg := CheckPassword(user, password, lang, enableCaptcha); msg != "" {
return nil, msg
}
}
@ -380,7 +391,7 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
return ""
}
func CheckToEnableCaptcha(application *Application) bool {
func CheckToEnableCaptcha(application *Application, organization, username string) bool {
if len(application.Providers) == 0 {
return false
}
@ -390,6 +401,10 @@ func CheckToEnableCaptcha(application *Application) bool {
continue
}
if providerItem.Provider.Category == "Captcha" {
if providerItem.Rule == "Dynamic" {
user := GetUserByFields(organization, username)
return user != nil && user.SigninWrongTimes >= SigninWrongTimesLimit
}
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)
}
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
user.SigninWrongTimes++
if user.SigninWrongTimes < SigninWrongTimesLimit {
user.SigninWrongTimes++
}
if user.SigninWrongTimes >= SigninWrongTimesLimit {
// record the latest failed login time
@ -57,10 +63,11 @@ func recordSigninErrorInfo(user *User, lang string) string {
// update user
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, user.IsGlobalAdmin)
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)
}
// 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()))
}

View File

@ -24,11 +24,9 @@ import (
func getDialer(provider *Provider) *gomail.Dialer {
dialer := &gomail.Dialer{}
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
if provider.Type == "SUBMAIL" {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.AppId, provider.ClientSecret)
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} else {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
}
dialer.SSL = !provider.DisableSsl
@ -40,14 +38,23 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
dialer := getDialer(provider)
message := gomail.NewMessage()
message.SetAddressHeader("From", provider.ClientId, sender)
fromAddress := provider.ClientId2
if fromAddress == "" {
fromAddress = provider.ClientId
}
fromName := provider.ClientSecret2
if fromName == "" {
fromName = sender
}
message.SetAddressHeader("From", fromAddress, fromName)
message.SetHeader("To", dest)
message.SetHeader("Subject", title)
message.SetBody("text/html", content)
if provider.Type == "Mailtrap" {
message.SkipUsernameCheck = true
}
message.SkipUsernameCheck = true
return dialer.DialAndSend(message)
}

View File

@ -67,6 +67,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
}

View File

@ -28,6 +28,7 @@ type Message struct {
Organization string `xorm:"varchar(100)" json:"organization"`
Chat string `xorm:"varchar(100) index" json:"chat"`
ReplyTo string `xorm:"varchar(100) index" json:"replyTo"`
Author string `xorm:"varchar(100)" json:"author"`
Text string `xorm:"mediumtext" json:"text"`
}
@ -143,6 +144,15 @@ func DeleteMessage(message *Message) bool {
return affected != 0
}
func DeleteChatMessages(chat string) bool {
affected, err := adapter.Engine.Delete(&Message{Chat: chat})
if err != nil {
panic(err)
}
return affected != 0
}
func (p *Message) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

107
object/mfa.go Normal file
View File

@ -0,0 +1,107 @@
// 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/beego/beego/context"
)
type MfaSessionData struct {
UserId string
}
type MfaProps struct {
Id string `json:"id"`
IsPreferred bool `json:"isPreferred"`
AuthType string `json:"type" form:"type"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
RecoveryCodes []string `json:"recoveryCodes,omitempty"`
}
type MfaInterface interface {
SetupVerify(ctx *context.Context, passCode string) error
Verify(passCode string) error
Initiate(ctx *context.Context, name1 string, name2 string) (*MfaProps, error)
Enable(ctx *context.Context, user *User) error
}
const (
SmsType = "sms"
TotpType = "app"
)
const (
MfaSessionUserId = "MfaSessionUserId"
NextMfa = "NextMfa"
)
func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
switch providerType {
case SmsType:
return NewSmsTwoFactor(config)
case TotpType:
return nil
}
return nil
}
func RecoverTfs(user *User, recoveryCode string) error {
hit := false
twoFactor := user.GetPreferMfa(false)
if len(twoFactor.RecoveryCodes) == 0 {
return fmt.Errorf("do not have recovery codes")
}
for _, code := range twoFactor.RecoveryCodes {
if code == recoveryCode {
hit = true
break
}
}
if !hit {
return fmt.Errorf("recovery code not found")
}
affected := UpdateUser(user.GetId(), user, []string{"two_factor_auth"}, user.IsAdminUser())
if !affected {
return fmt.Errorf("")
}
return nil
}
func GetMaskedProps(props *MfaProps) *MfaProps {
maskedProps := &MfaProps{
AuthType: props.AuthType,
Id: props.Id,
IsPreferred: props.IsPreferred,
}
if props.AuthType == SmsType {
if !util.IsEmailValid(props.Secret) {
maskedProps.Secret = util.GetMaskedPhone(props.Secret)
} else {
maskedProps.Secret = util.GetMaskedEmail(props.Secret)
}
}
return maskedProps
}

120
object/mfa_sms.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/beego/beego/context"
"github.com/google/uuid"
)
const (
MfaSmsCountryCodeSession = "mfa_country_code"
MfaSmsDestSession = "mfa_dest"
MfaSmsRecoveryCodesSession = "mfa_recovery_codes"
)
type SmsMfa struct {
Config *MfaProps
}
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
if countryCode != "" {
dest, _ = util.GetE164Number(dest, countryCode)
}
if result := CheckVerificationCode(dest, passCode, "en"); result.Code != VerificationSuccess {
return errors.New(result.Msg)
}
return nil
}
func (mfa *SmsMfa) Verify(passCode string) error {
if !util.IsEmailValid(mfa.Config.Secret) {
mfa.Config.Secret, _ = util.GetE164Number(mfa.Config.Secret, mfa.Config.CountryCode)
}
if result := CheckVerificationCode(mfa.Config.Secret, passCode, "en"); result.Code != VerificationSuccess {
return errors.New(result.Msg)
}
return nil
}
func (mfa *SmsMfa) Initiate(ctx *context.Context, name string, secret string) (*MfaProps, error) {
recoveryCode, err := uuid.NewRandom()
if err != nil {
return nil, err
}
err = ctx.Input.CruSession.Set(MfaSmsRecoveryCodesSession, []string{recoveryCode.String()})
if err != nil {
return nil, err
}
mfaProps := MfaProps{
AuthType: SmsType,
RecoveryCodes: []string{recoveryCode.String()},
}
return &mfaProps, nil
}
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
recoveryCodes := ctx.Input.CruSession.Get(MfaSmsRecoveryCodesSession).([]string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
if dest == "" || len(recoveryCodes) == 0 {
return fmt.Errorf("MFA dest or recovery codes is empty")
}
if !util.IsEmailValid(dest) {
mfa.Config.CountryCode = countryCode
}
mfa.Config.AuthType = SmsType
mfa.Config.Id = uuid.NewString()
mfa.Config.Secret = dest
mfa.Config.RecoveryCodes = recoveryCodes
for i, mfaProp := range user.MultiFactorAuths {
if mfaProp.Secret == mfa.Config.Secret {
user.MultiFactorAuths = append(user.MultiFactorAuths[:i], user.MultiFactorAuths[i+1:]...)
}
}
user.MultiFactorAuths = append(user.MultiFactorAuths, mfa.Config)
affected := UpdateUser(user.GetId(), user, []string{"multi_factor_auths"}, user.IsAdminUser())
if !affected {
return fmt.Errorf("failed to enable two factor authentication")
}
return nil
}
func NewSmsTwoFactor(config *MfaProps) *SmsMfa {
if config == nil {
config = &MfaProps{
AuthType: SmsType,
}
}
return &SmsMfa{
Config: config,
}
}

View File

@ -16,7 +16,6 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/i18n"
@ -50,7 +49,7 @@ type Organization struct {
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
@ -210,14 +209,14 @@ func GetAccountItemByName(name string, organization *Organization) *AccountItem
return nil
}
func CheckAccountItemModifyRule(accountItem *AccountItem, user *User, lang string) (bool, string) {
func CheckAccountItemModifyRule(accountItem *AccountItem, isAdmin bool, lang string) (bool, string) {
if accountItem == nil {
return true, ""
}
switch accountItem.ModifyRule {
case "Admin":
if user == nil || !user.IsAdmin && !user.IsGlobalAdmin {
if !isAdmin {
return false, fmt.Sprintf(i18n.Translate(lang, "organization:Only admin can modify the %s."), accountItem.Name)
}
case "Immutable":
@ -299,18 +298,16 @@ func organizationChangeTrigger(oldName string, newName string) error {
}
for i, u := range role.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
role.Users[i] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
role.Users[i] = util.GetId(owner, newName)
}
}
for i, u := range role.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
role.Roles[i] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
role.Roles[i] = util.GetId(owner, newName)
}
}
role.Owner = newName
@ -326,18 +323,16 @@ func organizationChangeTrigger(oldName string, newName string) error {
}
for i, u := range permission.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
permission.Users[i] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
permission.Users[i] = util.GetId(owner, newName)
}
}
for i, u := range permission.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
permission.Roles[i] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
permission.Roles[i] = util.GetId(owner, newName)
}
}
permission.Owner = newName

View File

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

View File

@ -118,35 +118,53 @@ func getPolicies(permission *Permission) [][]string {
return policies
}
func getRolesInRole(roleId string, visited map[string]struct{}) []*Role {
role := GetRole(roleId)
if role == nil {
return []*Role{}
}
visited[roleId] = struct{}{}
roles := []*Role{role}
for _, subRole := range role.Roles {
if _, ok := visited[subRole]; !ok {
roles = append(roles, getRolesInRole(subRole, visited)...)
}
}
return roles
}
func getGroupingPolicies(permission *Permission) [][]string {
var groupingPolicies [][]string
domainExist := len(permission.Domains) > 0
permissionId := permission.GetId()
for _, role := range permission.Roles {
roleObj := GetRole(role)
if roleObj == nil {
continue
}
for _, roleId := range permission.Roles {
visited := map[string]struct{}{}
rolesInRole := getRolesInRole(roleId, visited)
for _, subUser := range roleObj.Users {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subUser, role, domain, "", "", permissionId})
for _, role := range rolesInRole {
roleId := role.GetId()
for _, subUser := range role.Users {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subUser, roleId, domain, "", "", permissionId})
}
} else {
groupingPolicies = append(groupingPolicies, []string{subUser, roleId, "", "", "", permissionId})
}
} else {
groupingPolicies = append(groupingPolicies, []string{subUser, role, "", "", "", permissionId})
}
}
for _, subRole := range roleObj.Roles {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subRole, role, domain, "", "", permissionId})
for _, subRole := range role.Roles {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subRole, roleId, domain, "", "", permissionId})
}
} else {
groupingPolicies = append(groupingPolicies, []string{subRole, roleId, "", "", "", permissionId})
}
} else {
groupingPolicies = append(groupingPolicies, []string{subRole, role, "", "", "", permissionId})
}
}
}

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 {
Method string `json:"method"`
Name string `json:"name"`
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

@ -78,8 +78,11 @@ func GetMaskedProvider(provider *Provider) *Provider {
if provider.ClientSecret != "" {
provider.ClientSecret = "***"
}
if provider.ClientSecret2 != "" {
provider.ClientSecret2 = "***"
if provider.Category != "Email" {
if provider.ClientSecret2 != "" {
provider.ClientSecret2 = "***"
}
}
return provider
@ -177,8 +180,8 @@ func GetProvider(id string) *Provider {
return getProvider(owner, name)
}
func GetDefaultCaptchaProvider() *Provider {
provider := Provider{Owner: "admin", Category: "Captcha"}
func getDefaultAiProvider() *Provider {
provider := Provider{Owner: "admin", Category: "AI"}
existed, err := adapter.Engine.Get(&provider)
if err != nil {
panic(err)

View File

@ -16,7 +16,6 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
@ -95,10 +94,25 @@ func UpdateRole(id string, role *Role) bool {
return false
}
visited := map[string]struct{}{}
permissions := GetPermissionsByRole(id)
for _, permission := range permissions {
removeGroupingPolicies(permission)
removePolicies(permission)
visited[permission.GetId()] = struct{}{}
}
ancestorRoles := GetAncestorRoles(id)
for _, r := range ancestorRoles {
permissions := GetPermissionsByRole(r.GetId())
for _, permission := range permissions {
permissionId := permission.GetId()
if _, ok := visited[permissionId]; !ok {
removeGroupingPolicies(permission)
visited[permissionId] = struct{}{}
}
}
}
if name != role.Name {
@ -113,11 +127,25 @@ func UpdateRole(id string, role *Role) bool {
panic(err)
}
visited = map[string]struct{}{}
newRoleID := role.GetId()
permissions = GetPermissionsByRole(newRoleID)
for _, permission := range permissions {
addGroupingPolicies(permission)
addPolicies(permission)
visited[permission.GetId()] = struct{}{}
}
ancestorRoles = GetAncestorRoles(newRoleID)
for _, r := range ancestorRoles {
permissions := GetPermissionsByRole(r.GetId())
for _, permission := range permissions {
permissionId := permission.GetId()
if _, ok := visited[permissionId]; !ok {
addGroupingPolicies(permission)
visited[permissionId] = struct{}{}
}
}
}
return affected != 0
@ -154,7 +182,7 @@ func (role *Role) GetId() string {
func GetRolesByUser(userId string) []*Role {
roles := []*Role{}
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&roles)
err := adapter.Engine.Where("users like ?", "%"+userId+"\"%").Find(&roles)
if err != nil {
panic(err)
}
@ -182,13 +210,12 @@ func roleChangeTrigger(oldName string, newName string) error {
}
for _, role := range roles {
for j, u := range role.Roles {
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
role.Roles[j] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
role.Roles[j] = util.GetId(owner, newName)
}
}
_, err = session.Where("name=?", role.Name).Update(role)
_, err = session.Where("name=?", role.Name).And("owner=?", role.Owner).Update(role)
if err != nil {
return err
}
@ -202,13 +229,12 @@ func roleChangeTrigger(oldName string, newName string) error {
for _, permission := range permissions {
for j, u := range permission.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
permission.Roles[j] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
permission.Roles[j] = util.GetId(owner, newName)
}
}
_, err = session.Where("name=?", permission.Name).Update(permission)
_, err = session.Where("name=?", permission.Name).And("owner=?", permission.Owner).Update(permission)
if err != nil {
return err
}
@ -224,3 +250,64 @@ func GetMaskedRoles(roles []*Role) []*Role {
return roles
}
func GetRolesByNamePrefix(owner string, prefix string) []*Role {
roles := []*Role{}
err := adapter.Engine.Where("owner=? and name like ?", owner, prefix+"%").Find(&roles)
if err != nil {
panic(err)
}
return roles
}
func GetAncestorRoles(roleId string) []*Role {
var (
result []*Role
roleMap = make(map[string]*Role)
visited = make(map[string]bool)
)
owner, _ := util.GetOwnerAndNameFromIdNoCheck(roleId)
allRoles := GetRoles(owner)
for _, r := range allRoles {
roleMap[r.GetId()] = r
}
// Second, find all the roles that contain father roles
for _, r := range allRoles {
isContain, ok := visited[r.GetId()]
if isContain {
result = append(result, r)
} else if !ok {
rId := r.GetId()
visited[rId] = containsRole(r, roleId, roleMap, visited)
if visited[rId] {
result = append(result, r)
}
}
}
return result
}
// containsRole is a helper function to check if a slice of roles contains a specific roleId
func containsRole(role *Role, roleId string, roleMap map[string]*Role, visited map[string]bool) bool {
if isContain, ok := visited[role.GetId()]; ok {
return isContain
}
for _, subRole := range role.Roles {
if subRole == roleId {
return true
}
r, ok := roleMap[subRole]
if ok && containsRole(r, roleId, roleMap, visited) {
return true
}
}
return false
}

View File

@ -157,6 +157,7 @@ type User struct {
Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
MultiFactorAuths []*MfaProps `json:"multiFactorAuths"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
@ -401,6 +402,12 @@ func GetMaskedUser(user *User) *User {
manageAccount.Password = "***"
}
}
if user.MultiFactorAuths != nil {
for i, props := range user.MultiFactorAuths {
user.MultiFactorAuths[i] = GetMaskedProps(props)
}
}
return user
}
@ -425,7 +432,7 @@ func GetLastUser(owner string) *User {
return nil
}
func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) bool {
func UpdateUser(id string, user *User, columns []string, isAdmin bool) bool {
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
oldUser := getUser(owner, name)
if oldUser == nil {
@ -456,7 +463,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
"signin_wrong_times", "last_signin_wrong_time",
}
}
if isGlobalAdmin {
if isAdmin {
columns = append(columns, "name", "email", "phone", "country_code")
}
@ -657,13 +664,12 @@ func userChangeTrigger(oldName string, newName string) error {
for _, role := range roles {
for j, u := range role.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
role.Users[j] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
role.Users[j] = util.GetId(owner, newName)
}
}
_, err = session.Where("name=?", role.Name).Update(role)
_, err = session.Where("name=?", role.Name).And("owner=?", role.Owner).Update(role)
if err != nil {
return err
}
@ -677,13 +683,12 @@ func userChangeTrigger(oldName string, newName string) error {
for _, permission := range permissions {
for j, u := range permission.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
permission.Users[j] = split[0] + "/" + split[1]
owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName {
permission.Users[j] = util.GetId(owner, newName)
}
}
_, err = session.Where("name=?", permission.Name).Update(permission)
_, err = session.Where("name=?", permission.Name).And("owner=?", permission.Owner).Update(permission)
if err != nil {
return err
}
@ -735,3 +740,35 @@ func (user *User) refreshAvatar() bool {
return false
}
func (user *User) IsMfaEnabled() bool {
return len(user.MultiFactorAuths) > 0
}
func (user *User) GetPreferMfa(masked bool) *MfaProps {
if len(user.MultiFactorAuths) == 0 {
return nil
}
if masked {
if len(user.MultiFactorAuths) == 1 {
return GetMaskedProps(user.MultiFactorAuths[0])
}
for _, v := range user.MultiFactorAuths {
if v.IsPreferred {
return GetMaskedProps(v)
}
}
return GetMaskedProps(user.MultiFactorAuths[0])
} else {
if len(user.MultiFactorAuths) == 1 {
return user.MultiFactorAuths[0]
}
for _, v := range user.MultiFactorAuths {
if v.IsPreferred {
return v
}
}
return user.MultiFactorAuths[0]
}
}

View File

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

View File

@ -77,7 +77,7 @@ func getObject(ctx *context.Context) (string, string) {
body := ctx.Input.RequestBody
if len(body) == 0 {
return "", ""
return ctx.Request.Form.Get("owner"), ctx.Request.Form.Get("name")
}
var obj Object

View File

@ -0,0 +1,37 @@
package routers
import (
"fmt"
"strings"
"time"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
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))
}
func PrometheusFilter(ctx *context.Context) {
method := ctx.Input.Method()
path := ctx.Input.URL()
if strings.HasPrefix(path, "/api/metrics") {
systemInfo, err := util.GetSystemInfo()
if err == nil {
recordSystemInfo(systemInfo)
}
return
}
if strings.HasPrefix(path, "/api") {
ctx.Input.SetData("startTime", time.Now())
object.TotalThroughput.Inc()
object.ApiThroughput.WithLabelValues(path, method).Inc()
}
}

View File

@ -21,8 +21,8 @@ package routers
import (
"github.com/beego/beego"
"github.com/casdoor/casdoor/controllers"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func init() {
@ -57,6 +57,7 @@ func initAPI() {
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
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-organization", &controllers.ApiController{}, "GET:GetOrganization")
@ -155,7 +156,6 @@ func initAPI() {
beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken")
beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken")
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/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
beego.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken")
@ -197,6 +197,7 @@ func initAPI() {
beego.Router("/api/get-messages", &controllers.ApiController{}, "GET:GetMessages")
beego.Router("/api/get-message", &controllers.ApiController{}, "GET:GetMessage")
beego.Router("/api/get-message-answer", &controllers.ApiController{}, "GET:GetMessageAnswer")
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")
@ -237,6 +238,15 @@ func initAPI() {
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin")
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish")
beego.Router("/api/mfa/setup/initiate", &controllers.ApiController{}, "POST:MfaSetupInitiate")
beego.Router("/api/mfa/setup/verify", &controllers.ApiController{}, "POST:MfaSetupVerify")
beego.Router("/api/mfa/setup/enable", &controllers.ApiController{}, "POST:MfaSetupEnable")
beego.Router("/api/delete-mfa", &controllers.ApiController{}, "POST:DeleteMfa")
beego.Router("/api/set-preferred-mfa", &controllers.ApiController{}, "POST:SetPreferredMfa")
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo")
beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")
beego.Handler("/api/metrics", promhttp.Handler())
}

View File

@ -51,6 +51,12 @@ func StaticFilter(ctx *context.Context) {
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) {
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": {
"post": {
"tags": [
@ -107,6 +135,34 @@
"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": {
"post": {
"tags": [
@ -495,6 +551,32 @@
"operationId": "ApiController.GetCaptcha"
}
},
"/api/api/get-captcha-status": {
"get": {
"tags": [
"Token API"
],
"description": "Get Login Error Counts",
"operationId": "ApiController.GetCaptchaStatus",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of user",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/api/get-webhook-event": {
"get": {
"tags": [
@ -595,6 +677,14 @@
}
}
},
"/api/api/verify-code": {
"post": {
"tags": [
"Verification API"
],
"operationId": "ApiController.VerifyCode"
}
},
"/api/api/webhook": {
"post": {
"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": {
"post": {
"tags": [
@ -708,6 +826,34 @@
"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": {
"post": {
"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": {
"get": {
"tags": [
@ -1357,6 +1558,61 @@
"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": {
"get": {
"tags": [
@ -2514,7 +2770,7 @@
"description": "Login information",
"required": true,
"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": {
"post": {
"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": {
"post": {
"tags": [
@ -3064,6 +3294,41 @@
"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": {
"post": {
"tags": [
@ -3644,11 +3909,11 @@
}
},
"definitions": {
"2306.0xc0003a4480.false": {
"1183.0xc000455050.false": {
"title": "false",
"type": "object"
},
"2340.0xc0003a44b0.false": {
"1217.0xc000455080.false": {
"title": "false",
"type": "object"
},
@ -3660,6 +3925,10 @@
"title": "Response",
"type": "object"
},
"controllers.AuthForm": {
"title": "AuthForm",
"type": "object"
},
"controllers.EmailForm": {
"title": "EmailForm",
"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": {
"title": "Response",
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/2306.0xc0003a4480.false"
"$ref": "#/definitions/1183.0xc000455050.false"
},
"data2": {
"$ref": "#/definitions/2340.0xc0003a44b0.false"
"$ref": "#/definitions/1217.0xc000455080.false"
},
"msg": {
"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": {
"title": "Header",
"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": {
"title": "Model",
"type": "object",

View File

@ -64,11 +64,47 @@ paths:
description: The Response object
schema:
$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:
post:
tags:
- Account API
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:
post:
tags:
@ -319,6 +355,23 @@ paths:
tags:
- Login API
operationId: ApiController.GetCaptcha
/api/api/get-captcha-status:
get:
tags:
- Token API
description: Get Login Error Counts
operationId: ApiController.GetCaptchaStatus
parameters:
- in: query
name: id
description: The id ( owner/name ) of user
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/get-webhook-event:
get:
tags:
@ -385,6 +438,11 @@ paths:
description: object
schema:
$ref: '#/definitions/Response'
/api/api/verify-code:
post:
tags:
- Verification API
operationId: ApiController.VerifyCode
/api/api/webhook:
post:
tags:
@ -453,11 +511,47 @@ paths:
description: The Response object
schema:
$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:
post:
tags:
- Account API
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:
post:
tags:
@ -800,6 +894,42 @@ paths:
type: array
items:
$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:
get:
tags:
@ -880,6 +1010,42 @@ paths:
tags:
- Account API
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:
get:
tags:
@ -1644,7 +1810,7 @@ paths:
description: Login information
required: true
schema:
$ref: '#/definitions/controllers.RequestForm'
$ref: '#/definitions/controllers.AuthForm'
responses:
"200":
description: The Response object
@ -1690,48 +1856,6 @@ paths:
description: The Response object
schema:
$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:
post:
description: The introspection endpoint is an OAuth 2.0 endpoint that takes a
@ -2001,11 +2125,57 @@ paths:
description: The Response object
schema:
$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:
post:
tags:
- Account API
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:
post:
tags:
@ -2385,10 +2555,10 @@ paths:
schema:
$ref: '#/definitions/Response'
definitions:
2306.0xc0003a4480.false:
1183.0xc000455050.false:
title: "false"
type: object
2340.0xc0003a44b0.false:
1217.0xc000455080.false:
title: "false"
type: object
LaravelResponse:
@ -2397,6 +2567,9 @@ definitions:
Response:
title: Response
type: object
controllers.AuthForm:
title: AuthForm
type: object
controllers.EmailForm:
title: EmailForm
type: object
@ -2413,76 +2586,14 @@ definitions:
type: string
title:
type: string
controllers.RequestForm:
title: RequestForm
type: object
properties:
affiliation:
type: string
application:
type: string
autoSignin:
type: boolean
captchaToken:
type: string
captchaType:
type: string
clientId:
type: string
clientSecret:
type: string
code:
type: string
countryCode:
type: string
email:
type: string
emailCode:
type: string
firstName:
type: string
idCard:
type: string
lastName:
type: string
method:
type: string
name:
type: string
organization:
type: string
password:
type: string
phone:
type: string
phoneCode:
type: string
provider:
type: string
redirectUri:
type: string
region:
type: string
relayState:
type: string
samlRequest:
type: string
samlResponse:
type: string
state:
type: string
type:
type: string
username:
type: string
controllers.Response:
title: Response
type: object
properties:
data:
$ref: '#/definitions/2306.0xc0003a4480.false'
$ref: '#/definitions/1183.0xc000455050.false'
data2:
$ref: '#/definitions/2340.0xc0003a44b0.false'
$ref: '#/definitions/1217.0xc000455080.false'
msg:
type: string
name:
@ -2657,6 +2768,37 @@ definitions:
type: string
type:
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:
title: Header
type: object
@ -2710,6 +2852,24 @@ definitions:
type: string
username:
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:
title: Model
type: object

View File

@ -20,6 +20,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
@ -141,6 +142,16 @@ func GenerateSimpleTimeId() string {
return t
}
func GetRandomName() string {
rand.Seed(time.Now().UnixNano())
const charset = "0123456789abcdefghijklmnopqrstuvwxyz"
result := make([]byte, 6)
for i := range result {
result[i] = charset[rand.Intn(len(charset))]
}
return string(result)
}
func GetId(owner, name string) string {
return fmt.Sprintf("%s/%s", owner, name)
}

View File

@ -25,6 +25,20 @@ func GetCurrentTime() string {
return tm.Format(time.RFC3339)
}
func GetCurrentTimeEx(timestamp string) string {
tm := time.Now()
inputTime, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
panic(err)
}
if !tm.After(inputTime) {
tm = inputTime.Add(1 * time.Millisecond)
}
return tm.Format("2006-01-02T15:04:05.999Z07:00")
}
func GetCurrentUnixTime() string {
return strconv.FormatInt(time.Now().UnixNano(), 10)
}

View File

@ -15,6 +15,7 @@
package util
import (
"fmt"
"net/mail"
"regexp"
@ -48,3 +49,24 @@ func GetE164Number(phone string, countryCode string) (string, bool) {
phoneNumber, _ := phonenumbers.Parse(phone, countryCode)
return phonenumbers.Format(phoneNumber, phonenumbers.E164), phonenumbers.IsValidNumber(phoneNumber)
}
func GetCountryCode(prefix string, phone string) (string, error) {
if prefix == "" || phone == "" {
return "", nil
}
phoneNumber, err := phonenumbers.Parse(fmt.Sprintf("+%s%s", prefix, phone), "")
if err != nil {
return "", err
}
if err != nil {
return "", err
}
countryCode := phonenumbers.GetRegionCodeForNumber(phoneNumber)
if countryCode == "" {
return "", fmt.Errorf("country code not found for phone prefix: %s", prefix)
}
return countryCode, nil
}

View File

@ -82,6 +82,12 @@
}
],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"react/no-unknown-property": [
"error",
{
"ignore": ["css"]
}
],
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"error",
@ -98,7 +104,6 @@
"no-console": "error",
"eqeqeq": "error",
"keyword-spacing": "error",
"react/prop-types": "off",
"react/display-name": "off",
"react/react-in-jsx-scope": "off",

View File

@ -24,6 +24,7 @@
"i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19",
"moment": "^2.29.1",
"qrcode.react": "^3.1.0",
"qs": "^6.10.2",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
@ -76,7 +77,6 @@
"eslint-plugin-react": "^7.31.1",
"husky": "^4.3.8",
"lint-staged": "^13.0.3",
"path-browserify": "^1.0.1",
"stylelint": "^14.11.0",
"stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^28.0.0"

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as AdapterBackend from "./backend/AdapterBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class AdapterListPage extends BaseListPage {
newAdapter() {
@ -225,7 +225,7 @@ class AdapterListPage extends BaseListPage {
return (
<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={() => (
<div>
{i18next.t("general:Adapters")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -62,7 +62,6 @@ import * as Conf from "./Conf";
import * as Auth from "./auth/Auth";
import EntryPage from "./EntryPage";
import ResultPage from "./auth/ResultPage";
import * as AuthBackend from "./auth/AuthBackend";
import AuthCallback from "./auth/AuthCallback";
import LanguageSelect from "./common/select/LanguageSelect";
@ -77,6 +76,7 @@ import AdapterEditPage from "./AdapterEditPage";
import {withTranslation} from "react-i18next";
import ThemeSelect from "./common/select/ThemeSelect";
import SessionListPage from "./SessionListPage";
import MfaSetupPage from "./auth/MfaSetupPage";
const {Header, Footer, Content} = Layout;
@ -517,8 +517,6 @@ class App extends Component {
renderRouter() {
return (
<Switch>
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/" render={(props) => this.renderLoginIfNotLoggedIn(<HomePage account={this.state.account} {...props} />)} />
<Route exact path="/account" render={(props) => this.renderLoginIfNotLoggedIn(<AccountPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} />
@ -563,6 +561,7 @@ class App extends Component {
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/mfa-authentication/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
@ -665,6 +664,7 @@ class App extends Component {
window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/forget") ||
window.location.pathname.startsWith("/prompt") ||
window.location.pathname.startsWith("/result") ||
window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/auto-signup");
}

View File

@ -767,7 +767,15 @@ class ApplicationEditPage extends React.Component {
renderSignupSigninPreview() {
const themeData = this.state.application.themeData ?? Conf.ThemeDefault;
let signUpUrl = `/signup/${this.state.application.name}`;
const signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`;
let redirectUri;
if (this.state.application.redirectUris.length !== 0) {
redirectUri = this.state.application.redirectUris[0];
} else {
redirectUri = "\"ERROR: You must specify at least one Redirect URL in 'Redirect URLs'\"";
}
const signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=read&state=casdoor`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"};
if (!this.state.application.enablePassword) {
signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize");

View File

@ -21,7 +21,7 @@ import * as Setting from "./Setting";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ApplicationListPage extends BaseListPage {
constructor(props) {
@ -254,7 +254,7 @@ class ApplicationListPage extends BaseListPage {
return (
<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={() => (
<div>
{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 i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class CertListPage extends BaseListPage {
newCert() {

View File

@ -13,8 +13,9 @@
// limitations under the License.
import React from "react";
import {Avatar, Input, List} from "antd";
import {Alert, Avatar, Input, List, Spin} from "antd";
import {CopyOutlined, DislikeOutlined, LikeOutlined, SendOutlined} from "@ant-design/icons";
import i18next from "i18next";
const {TextArea} = Input;
@ -29,7 +30,7 @@ class ChatBox extends React.Component {
}
componentDidUpdate(prevProps) {
if (prevProps.messages !== this.props.messages) {
if (prevProps.messages !== this.props.messages && this.props.messages !== undefined && this.props.messages !== null) {
this.scrollToListItem(this.props.messages.length);
}
}
@ -73,12 +74,30 @@ class ChatBox extends React.Component {
this.setState({inputValue: ""});
};
renderText(text) {
const lines = text.split("\n").map((line, index) => (
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
));
return <div>{lines}</div>;
}
renderList() {
if (this.props.messages === undefined || this.props.messages === null) {
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "20%"}} />
</div>
);
}
return (
<div ref={this.listContainerRef} style={{position: "relative", maxHeight: "calc(100vh - 140px)", overflowY: "auto"}}>
<List
itemLayout="horizontal"
dataSource={this.props.messages === undefined ? undefined : [...this.props.messages, {}]}
dataSource={[...this.props.messages, {}]}
renderItem={(item, index) => {
if (Object.keys(item).length === 0 && item.constructor === Object) {
return <List.Item id={`chatbox-list-item-${index}`} style={{
@ -98,7 +117,15 @@ class ChatBox extends React.Component {
<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>}
title={
<div style={{fontSize: "16px", fontWeight: "normal", lineHeight: "24px", marginTop: "-15px", marginLeft: "5px", marginRight: "80px"}}>
{
!item.text.includes("#ERROR#") ? this.renderText(item.text) : (
<Alert message={item.text.slice("#ERROR#: ".length)} type="error" showIcon />
)
}
</div>
}
/>
<div style={{position: "absolute", top: "0px", right: "0px"}}
>

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as ChatBackend from "./backend/ChatBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ChatListPage extends BaseListPage {
newChat() {
@ -241,7 +241,7 @@ class ChatListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={chats} rowKey="name" size="middle" bordered pagination={paginationProps}
<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;

View File

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

View File

@ -24,7 +24,13 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ChatPage extends BaseListPage {
newChat() {
constructor(props) {
super(props);
this.menu = React.createRef();
}
newChat(chat) {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.applicationName,
@ -33,8 +39,8 @@ class ChatPage extends BaseListPage {
updatedTime: moment().format(),
organization: this.props.account.owner,
displayName: `New Chat - ${randomName}`,
type: "Single",
category: "Chat Category - 1",
type: "AI",
category: chat !== undefined ? chat.category : "Chat Category - 1",
user1: `${this.props.account.owner}/${this.props.account.name}`,
user2: "",
users: [`${this.props.account.owner}/${this.props.account.name}`],
@ -42,40 +48,6 @@ class ChatPage extends BaseListPage {
};
}
// addChat() {
// const newChat = this.newChat();
// ChatBackend.addChat(newChat)
// .then((res) => {
// if (res.status === "ok") {
// this.props.history.push({pathname: `/chats/${newChat.name}`, mode: "add"});
// Setting.showMessage("success", i18next.t("general:Successfully added"));
// } else {
// Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
// }
// })
// .catch(error => {
// Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
// });
// }
//
// deleteChat(i) {
// ChatBackend.deleteChat(this.state.data[i])
// .then((res) => {
// if (res.status === "ok") {
// Setting.showMessage("success", i18next.t("general:Successfully deleted"));
// this.setState({
// data: Setting.deleteRow(this.state.data, i),
// pagination: {total: this.state.pagination.total - 1},
// });
// } else {
// Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
// }
// })
// .catch(error => {
// Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
// });
// }
newMessage(text) {
const randomName = Setting.getRandomName();
return {
@ -84,6 +56,7 @@ class ChatPage extends BaseListPage {
createdTime: moment().format(),
organization: this.props.account.owner,
chat: this.state.chatName,
replyTo: "",
author: `${this.props.account.owner}/${this.props.account.name}`,
text: text,
};
@ -111,47 +84,158 @@ class ChatPage extends BaseListPage {
messages: messages,
});
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage.author === "AI" && lastMessage.replyTo !== "" && lastMessage.text === "") {
let text = "";
MessageBackend.getMessageAnswer(lastMessage.owner, lastMessage.name, (data) => {
if (data === "") {
data = "\n";
}
const lastMessage2 = Setting.deepCopy(lastMessage);
text += data;
lastMessage2.text = text;
messages[messages.length - 1] = lastMessage2;
this.setState({
messages: messages,
});
}, (error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to get answer")}: ${error}`);
const lastMessage2 = Setting.deepCopy(lastMessage);
lastMessage2.text = `#ERROR#: ${error}`;
messages[messages.length - 1] = lastMessage2;
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}, false);
} 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: [],
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}`);
});
}
getCurrentChat() {
return this.state.data.filter(chat => chat.name === this.state.chatName)[0];
}
renderTable(chats) {
return (this.state.loading) ? <Spin size="large" style={{marginLeft: "50%", marginTop: "10%"}} /> : (
(
<div style={{display: "flex", height: "calc(100vh - 140px)"}}>
<div style={{width: "250px", height: "100%", backgroundColor: "white", borderRight: "1px solid rgb(245,245,245)"}}>
<ChatMenu chats={chats} onSelect={(i) => {
const chat = chats[i];
this.getMessages(chat.name);
this.setState({
chatName: chat.name,
});
}} />
</div>
<div style={{flex: 1, height: "100%", backgroundColor: "white", position: "relative"}}>
<div style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: "url(https://cdn.casbin.org/img/casdoor-logo_1185x256.png)",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "200px auto",
backgroundBlendMode: "luminosity",
filter: "grayscale(80%) brightness(140%) contrast(90%)",
opacity: 0.5,
}}>
</div>
<ChatBox messages={this.state.messages} sendMessage={(text) => {this.sendMessage(text);}} account={this.props.account} />
</div>
const onSelectChat = (i) => {
const chat = chats[i];
this.setState({
chatName: chat.name,
messages: null,
});
this.getMessages(chat.name);
};
const onAddChat = () => {
const chat = this.getCurrentChat();
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)", borderBottom: "1px solid rgb(245,245,245)"}}>
<ChatMenu ref={this.menu} chats={chats} onSelectChat={onSelectChat} onAddChat={onAddChat} onDeleteChat={onDeleteChat} />
</div>
<div style={{flex: 1, height: "100%", backgroundColor: "white", position: "relative"}}>
{
(this.state.messages === undefined || 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,
pointerEvents: "none",
}}>
</div>
)
}
<ChatBox messages={this.state.messages} sendMessage={(text) => {this.sendMessage(text);}} account={this.props.account} />
</div>
</div>
);
}
fetch = (params = {}) => {
fetch = (params = {}, setLoading = true) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
@ -161,13 +245,16 @@ class ChatPage extends BaseListPage {
field = "type";
value = params.type;
}
this.setState({loading: true});
ChatBackend.getChats("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
if (setLoading) {
this.setState({loading: true});
}
ChatBackend.getChats("admin", params.pagination.current, -1, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
messages: [],
pagination: {
...params.pagination,
total: res.data2,
@ -184,6 +271,10 @@ class ChatPage extends BaseListPage {
chatName: chat.name,
});
}
if (!setLoading) {
this.menu.current.setSelectedKeyToNewChat(chats);
}
} else {
if (Setting.isResponseDenied(res)) {
this.setState({

View File

@ -24,7 +24,9 @@ import LoginPage from "./auth/LoginPage";
import SelfForgetPage from "./auth/SelfForgetPage";
import ForgetPage from "./auth/ForgetPage";
import PromptPage from "./auth/PromptPage";
import ResultPage from "./auth/ResultPage";
import CasLogout from "./auth/CasLogout";
import {authConfig} from "./auth/Auth";
class EntryPage extends React.Component {
constructor(props) {
@ -53,10 +55,6 @@ class EntryPage extends React.Component {
}
}
getApplicationObj() {
return this.state.application || null;
}
render() {
const onUpdateApplication = (application) => {
this.setState({
@ -71,7 +69,7 @@ class EntryPage extends React.Component {
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
<Spin size="large" spinning={this.state.application === undefined} tip={i18next.t("login:Loading")} style={{margin: "0 auto"}} />
<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="/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} />)} />
@ -83,6 +81,8 @@ class EntryPage extends React.Component {
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...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="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...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={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} />
</Switch>

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as MessageBackend from "./backend/MessageBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class MessageListPage extends BaseListPage {
newMessage() {
@ -31,6 +31,7 @@ class MessageListPage extends BaseListPage {
createdTime: moment().format(),
organization: this.props.account.owner,
chat: "",
replyTo: "",
author: `${this.props.account.owner}/${this.props.account.name}`,
text: "",
};
@ -183,7 +184,7 @@ class MessageListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={messages} rowKey="name" size="middle" bordered pagination={paginationProps}
<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;

View File

@ -18,7 +18,11 @@ import * as ModelBackend from "./backend/ModelBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import TextArea from "antd/es/input/TextArea";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/mode/properties/properties");
const {Option} = Select;
@ -135,9 +139,15 @@ class ModelEditPage extends React.Component {
{Setting.getLabel(i18next.t("model:Model text"), i18next.t("model:Model text - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={10} value={this.state.model.modelText} onChange={e => {
this.updateModelField("modelText", e.target.value);
}} />
<div style={{width: "100%"}} >
<CodeMirror
value={this.state.model.modelText}
options={{mode: "properties", theme: "default"}}
onBeforeChange={(editor, data, value) => {
this.updateModelField("modelText", value);
}}
/>
</div>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >

View File

@ -20,7 +20,22 @@ import * as Setting from "./Setting";
import * as ModelBackend from "./backend/ModelBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
const rbacModel = `[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act`;
class ModelListPage extends BaseListPage {
newModel() {
@ -30,7 +45,7 @@ class ModelListPage extends BaseListPage {
name: `model_${randomName}`,
createdTime: moment().format(),
displayName: `New Model - ${randomName}`,
modelText: "",
modelText: rbacModel,
isEnabled: true,
};
}
@ -163,7 +178,7 @@ class ModelListPage extends BaseListPage {
return (
<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}
title={() => (
<div>

View File

@ -362,6 +362,8 @@ class OrganizationEditPage extends React.Component {
submitOrganizationEdit(willExist) {
const organization = Setting.deepCopy(this.state.organization);
organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item");
OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization)
.then((res) => {
if (res.status === "ok") {

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class OrganizationListPage extends BaseListPage {
newOrganization() {
@ -63,6 +63,7 @@ class OrganizationListPage extends BaseListPage {
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is global admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},

View File

@ -21,7 +21,7 @@ import * as PaymentBackend from "./backend/PaymentBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import * as Provider from "./auth/Provider";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class PaymentListPage extends BaseListPage {
newPayment() {
@ -243,7 +243,7 @@ class PaymentListPage extends BaseListPage {
return (
<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={() => (
<div>
{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 i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class PermissionListPage extends BaseListPage {
newPermission() {
@ -321,7 +321,7 @@ class PermissionListPage extends BaseListPage {
return (
<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={() => (
<div>
{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 BaseListPage from "./BaseListPage";
import {EditOutlined} from "@ant-design/icons";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ProductListPage extends BaseListPage {
newProduct() {

View File

@ -125,11 +125,41 @@ class ProviderEditPage extends React.Component {
} else {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
}
case "AI":
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
}
getClientId2Label(provider) {
switch (provider.category) {
case "Email":
return Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"));
default:
if (provider.type === "Aliyun Captcha") {
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
} else if (provider.type === "WeChat Pay") {
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
}
}
}
getClientSecret2Label(provider) {
switch (provider.category) {
case "Email":
return Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"));
default:
if (provider.type === "Aliyun Captcha") {
return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"));
}
}
}
getProviderSubTypeOptions(type) {
if (type === "WeCom" || type === "Infoflow") {
return (
@ -278,17 +308,20 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Alipay");
} else if (value === "Captcha") {
this.updateProviderField("type", "Default");
} else if (value === "AI") {
this.updateProviderField("type", "OpenAI API - GPT");
}
})}>
{
[
{id: "OAuth", name: "OAuth"},
{id: "AI", name: "AI"},
{id: "Captcha", name: "Captcha"},
{id: "Email", name: "Email"},
{id: "OAuth", name: "OAuth"},
{id: "Payment", name: "Payment"},
{id: "SAML", name: "SAML"},
{id: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"},
{id: "SAML", name: "SAML"},
{id: "Payment", name: "Payment"},
{id: "Captcha", name: "Captcha"},
]
.sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
@ -437,19 +470,23 @@ class ProviderEditPage extends React.Component {
{
this.state.provider.category === "Captcha" && this.state.provider.type === "Default" ? null : (
<React.Fragment>
{
this.state.provider.category === "AI" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel(this.state.provider)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField("clientId", e.target.value);
}} />
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel(this.state.provider)}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField("clientId", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecretLabel(this.state.provider)}
{this.getClientSecretLabel(this.state.provider)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret} onChange={e => {
@ -461,15 +498,11 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" ? null : (
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.state.provider.type === "Aliyun Captcha"
? Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - 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"))}
{this.getClientId2Label(this.state.provider)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId2} onChange={e => {
@ -481,9 +514,7 @@ class ProviderEditPage extends React.Component {
this.state.provider.type === "WeChat Pay" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.state.provider.type === "Aliyun Captcha"
? Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"))
: Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"))}
{this.getClientSecret2Label(this.state.provider)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret2} onChange={e => {

View File

@ -21,7 +21,7 @@ import * as ProviderBackend from "./backend/ProviderBackend";
import * as Provider from "./auth/Provider";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ProviderListPage extends BaseListPage {
constructor(props) {
@ -227,7 +227,7 @@ class ProviderListPage extends BaseListPage {
return (
<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={() => (
<div>
{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 {Link} from "react-router-dom";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ResourceListPage extends BaseListPage {
constructor(props) {

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as RoleBackend from "./backend/RoleBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class RoleListPage extends BaseListPage {
newRole() {
@ -196,7 +196,7 @@ class RoleListPage extends BaseListPage {
return (
<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={() => (
<div>
{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 React from "react";
import * as SessionBackend from "./backend/SessionBackend";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class SessionListPage extends BaseListPage {
@ -118,7 +118,7 @@ class SessionListPage extends BaseListPage {
return (
<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}
onChange={this.handleTableChange}
/>

View File

@ -24,7 +24,6 @@ import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet";
import * as Conf from "./Conf";
import * as phoneNumber from "libphonenumber-js";
import * as path from "path-browserify";
const {Option} = Select;
@ -205,6 +204,12 @@ export const OtherProviderInfo = {
url: "https://www.cloudflare.com/products/turnstile/",
},
},
AI: {
"OpenAI API - GPT": {
logo: `${StaticBaseUrl}/img/social_openai.svg`,
url: "https://platform.openai.com",
},
},
};
export function initCountries() {
@ -856,6 +861,10 @@ export function getProviderTypeOptions(category) {
{id: "GEETEST", name: "GEETEST"},
{id: "Cloudflare Turnstile", name: "Cloudflare Turnstile"},
]);
} else if (category === "AI") {
return ([
{id: "OpenAI API - GPT", name: "OpenAI API - GPT"},
]);
} else {
return [];
}
@ -888,7 +897,7 @@ export function getLoginLink(application) {
} else if (authConfig.appName === application.name) {
url = "/login";
} else if (application.signinUrl === "") {
url = path.join(application.homepageUrl, "/login");
url = trim(application.homepageUrl, "/") + "/login";
} else {
url = application.signinUrl;
}
@ -902,10 +911,11 @@ export function renderLoginLink(application, text) {
export function redirectToLoginPage(application, history) {
const loginLink = getLoginLink(application);
if (loginLink.indexOf("http") === 0 || loginLink.indexOf("https") === 0) {
window.location.replace(loginLink);
if (loginLink.startsWith("http://") || loginLink.startsWith("https://")) {
goToLink(loginLink);
} else {
history.push(loginLink);
}
history.push(loginLink);
}
function renderLink(url, text, onClick) {
@ -1047,13 +1057,17 @@ export function getMaskedEmail(email) {
return `${username}@${domainTokens.join(".")}`;
}
export function IsEmail(s) {
return s.includes("@");
}
export function getArrayItem(array, key, value) {
const res = array.filter(item => item[key] === value)[0];
return res;
}
export function getDeduplicatedArray(array, filterArray, key) {
const res = array.filter(item => filterArray.filter(filterItem => filterItem[key] === item[key]).length === 0);
const res = array.filter(item => !filterArray.some(tableItem => tableItem[key] === item[key]));
return res;
}

View File

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

View File

@ -20,7 +20,7 @@ import * as Setting from "./Setting";
import * as TokenBackend from "./backend/TokenBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class TokenListPage extends BaseListPage {
newToken() {
@ -222,7 +222,7 @@ class TokenListPage extends BaseListPage {
return (
<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={() => (
<div>
{i18next.t("general:Tokens")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Result, Row, Select, Spin, Switch} from "antd";
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Spin, Switch, Tag} from "antd";
import * as UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
@ -30,6 +30,11 @@ import WebAuthnCredentialTable from "./table/WebauthnCredentialTable";
import ManagedAccountTable from "./table/ManagedAccountTable";
import PropertyTable from "./table/propertyTable";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import {DeleteMfa} from "./backend/MfaBackend";
import {CheckCircleOutlined} from "@ant-design/icons";
import {SmsMfaType} from "./auth/MfaSetupPage";
import * as MfaBackend from "./backend/MfaBackend";
const {Option} = Select;
@ -64,6 +69,7 @@ class UserEditPage extends React.Component {
if (data.status === null || data.status !== "error") {
this.setState({
user: data,
multiFactorAuths: data?.multiFactorAuths ?? [],
});
}
this.setState({
@ -142,6 +148,58 @@ class UserEditPage extends React.Component {
return this.props.account.countryCode;
}
getMfaProps(type = "") {
if (!(this.state.multiFactorAuths?.length > 0)) {
return [];
}
if (type === "") {
return this.state.multiFactorAuths;
}
return this.state.multiFactorAuths.filter(mfaProps => mfaProps.type === type);
}
loadMore = (table, type) => {
return <div
style={{
textAlign: "center",
marginTop: 12,
height: 32,
lineHeight: "32px",
}}
>
<Button onClick={() => {
this.setState({
multiFactorAuths: Setting.addRow(table, {"type": type}),
});
}}>{i18next.t("general:Add")}</Button>
</div>;
};
deleteMfa = (id) => {
this.setState({
RemoveMfaLoading: true,
});
DeleteMfa({
id: id,
owner: this.state.user.owner,
name: this.state.user.name,
}).then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
multiFactorAuths: res.data,
});
} else {
Setting.showMessage("error", i18next.t("general:Failed to delete"));
}
}).finally(() => {
this.setState({
RemoveMfaLoading: false,
});
});
};
renderAccountItem(accountItem) {
if (!accountItem.visible) {
return null;
@ -180,6 +238,12 @@ class UserEditPage extends React.Component {
disabled = true;
}
if (accountItem.name === "Organization" || accountItem.name === "Name") {
if (this.state.user.owner === "built-in" && this.state.user.name === "admin") {
disabled = true;
}
}
if (accountItem.name === "Organization") {
return (
<Row style={{marginTop: "10px"}} >
@ -689,6 +753,74 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Multi-factor authentication") {
return (
!this.isSelfOrAdmin() ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} :
</Col>
<Col span={22} >
<Card title={i18next.t("mfa:Multi-factor methods")}>
<Card type="inner" title={i18next.t("mfa:SMS/Email message")}>
<List
itemLayout="horizontal"
dataSource={this.getMfaProps(SmsMfaType)}
loadMore={this.loadMore(this.state.multiFactorAuths, SmsMfaType)}
renderItem={(item, index) => (
<List.Item>
<div>
{item?.id === undefined ?
<Button type={"default"} onClick={() => {
Setting.goToLink("/mfa-authentication/setup");
}}>
{i18next.t("mfa:Setup")}
</Button> :
<Tag icon={<CheckCircleOutlined />} color="success">
{i18next.t("general:Enabled")}
</Tag>
}
{item.secret}
</div>
{item?.id === undefined ? null :
<div>
{item.isPreferred ?
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
{i18next.t("mfa:preferred")}
</Tag> :
<Button type="primary" style={{marginRight: 20}} onClick={() => {
const values = {
owner: this.state.user.owner,
name: this.state.user.name,
id: item.id,
};
MfaBackend.SetPreferredMfa(values).then((res) => {
if (res.status === "ok") {
this.setState({
multiFactorAuths: res.data,
});
}
});
}}>
{i18next.t("mfa:Set preferred")}
</Button>
}
<PopconfirmModal
title={i18next.t("general:Sure to delete") + "?"}
onConfirm={() => this.deleteMfa(item.id)}
>
</PopconfirmModal>
</div>
}
</List.Item>
)}
/>
</Card>
</Card>
</Col>
</Row>
)
);
} else if (accountItem.name === "WebAuthn credentials") {
return (
<Row style={{marginTop: "20px"}} >

View File

@ -22,7 +22,7 @@ import * as Setting from "./Setting";
import * as UserBackend from "./backend/UserBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class UserListPage extends BaseListPage {
constructor(props) {
@ -339,7 +339,7 @@ class UserListPage extends BaseListPage {
width: "190px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name);
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name) || (record.owner === "built-in" && record.name === "admin");
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => {

View File

@ -244,7 +244,7 @@ class WebhookEditPage extends React.Component {
}} >
{
(
["signup", "login", "logout", "add-user", "update-user", "add-organization", "update-organization", "add-provider", "update-provider"].map((option, index) => {
["signup", "login", "logout", "add-user", "update-user", "delete-user", "add-organization", "update-organization", "delete-organization", "add-application", "update-application", "delete-application", "add-provider", "update-provider", "delete-provider"].map((option, index) => {
return (
<Option key={option} value={option}>{option}</Option>
);

View File

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

View File

@ -48,7 +48,7 @@ export function getEmailAndPhone(organization, username) {
export function oAuthParamsToQuery(oAuthParams) {
// login
if (oAuthParams === null) {
if (oAuthParams === null || oAuthParams === undefined) {
return "";
}
@ -139,3 +139,13 @@ export function getWechatMessageEvent() {
},
}).then(res => res.json());
}
export function getCaptchaStatus(values) {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&user_id=${values["username"]}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@ -31,7 +31,9 @@ import CustomGithubCorner from "../common/CustomGithubCorner";
import {SendCodeInput} from "../common/SendCodeInput";
import LanguageSelect from "../common/select/LanguageSelect";
import {CaptchaModal} from "../common/modal/CaptchaModal";
import {CaptchaRule} from "../common/modal/CaptchaModal";
import RedirectForm from "../common/RedirectForm";
import {MfaAuthVerifyForm, NextMfa} from "./MfaAuthVerifyForm";
class LoginPage extends React.Component {
constructor(props) {
@ -47,7 +49,7 @@ class LoginPage extends React.Component {
validEmailOrPhone: false,
validEmail: false,
loginMethod: "password",
enableCaptchaModal: false,
enableCaptchaModal: CaptchaRule.Never,
openCaptchaModal: false,
verifyCaptcha: undefined,
samlResponse: "",
@ -81,7 +83,13 @@ class LoginPage extends React.Component {
if (prevProps.application !== this.props.application) {
const captchaProviderItems = this.getCaptchaProviderItems(this.props.application);
if (captchaProviderItems) {
this.setState({enableCaptchaModal: captchaProviderItems.some(providerItem => providerItem.rule === "Always")});
if (captchaProviderItems.some(providerItem => providerItem.rule === "Always")) {
this.setState({enableCaptchaModal: CaptchaRule.Always});
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) {
this.setState({enableCaptchaModal: CaptchaRule.Dynamic});
} else {
this.setState({enableCaptchaModal: CaptchaRule.Never});
}
}
if (this.props.account && this.props.account.owner === this.props.application?.organization) {
@ -110,6 +118,22 @@ class LoginPage extends React.Component {
}
}
checkCaptchaStatus(values) {
AuthBackend.getCaptchaStatus(values)
.then((res) => {
if (res.status === "ok") {
if (res.data) {
this.setState({
openCaptchaModal: true,
values: values,
});
return null;
}
}
this.login(values);
});
}
getApplicationLogin() {
const oAuthParams = Util.getOAuthGetParameters();
AuthBackend.getApplicationLogin(oAuthParams)
@ -124,7 +148,6 @@ class LoginPage extends React.Component {
});
}
});
return null;
}
getApplication() {
@ -255,15 +278,19 @@ class LoginPage extends React.Component {
this.signInWithWebAuthn(username, values);
return;
}
if (this.state.loginMethod === "password" && this.state.enableCaptchaModal) {
this.setState({
openCaptchaModal: true,
values: values,
});
} else {
this.login(values);
if (this.state.loginMethod === "password") {
if (this.state.enableCaptchaModal === CaptchaRule.Always) {
this.setState({
openCaptchaModal: true,
values: values,
});
return;
} else if (this.state.enableCaptchaModal === CaptchaRule.Dynamic) {
this.checkCaptchaStatus(values);
return;
}
}
this.login(values);
}
login(values) {
@ -297,7 +324,7 @@ class LoginPage extends React.Component {
this.populateOauthValues(values);
AuthBackend.login(values, oAuthParams)
.then((res) => {
if (res.status === "ok") {
const callback = (res) => {
const responseType = values["type"];
if (responseType === "login") {
@ -308,8 +335,9 @@ class LoginPage extends React.Component {
} else if (responseType === "code") {
this.postCodeLoginAction(res);
} else if (responseType === "token" || responseType === "id_token") {
const amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "saml") {
if (res.data2.method === "POST") {
this.setState({
@ -323,6 +351,25 @@ class LoginPage extends React.Component {
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
}
}
};
if (res.status === "ok") {
callback();
} else if (res.status === NextMfa) {
this.setState({
getVerifyTotp: () => {
return (
<MfaAuthVerifyForm
mfaProps={res.data}
formValues={values}
oAuthParams={oAuthParams}
application={this.getApplicationObj()}
onFail={() => {
Setting.showMessage("error", i18next.t("mfa:Verification failed"));
}}
onSuccess={(res) => callback(res)}
/>);
},
});
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@ -544,13 +591,15 @@ class LoginPage extends React.Component {
}
renderCaptchaModal(application) {
if (!this.state.enableCaptchaModal) {
if (this.state.enableCaptchaModal === CaptchaRule.Never) {
return null;
}
const provider = this.getCaptchaProviderItems(application)
.filter(providerItem => providerItem.rule === "Always")
.map(providerItem => providerItem.provider)[0];
const captchaProviderItems = this.getCaptchaProviderItems(application);
const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always");
const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic");
const provider = alwaysProviderItems.length > 0
? alwaysProviderItems[0].provider
: dynamicProviderItems[0].provider;
return <CaptchaModal
owner={provider.owner}
@ -571,33 +620,20 @@ class LoginPage extends React.Component {
}
renderFooter(application) {
if (this.state.mode === "signup") {
return (
<div style={{float: "right"}}>
{i18next.t("signup:Have account?")}&nbsp;
{
Setting.renderLoginLink(application, i18next.t("signup:sign in now"))
}
</div>
);
} else {
return (
<React.Fragment>
<span style={{float: "right"}}>
{
!application.enableSignUp ? null : (
<React.Fragment>
{i18next.t("login:No account?")}&nbsp;
{
Setting.renderSignupLink(application, i18next.t("login:sign up now"))
}
</React.Fragment>
)
}
</span>
</React.Fragment>
);
}
return (
<span style={{float: "right"}}>
{
!application.enableSignUp ? null : (
<React.Fragment>
{i18next.t("login:No account?")}&nbsp;
{
Setting.renderSignupLink(application, i18next.t("login:sign up now"))
}
</React.Fragment>
)
}
</span>
);
}
sendSilentSigninData(data) {
@ -746,13 +782,9 @@ class LoginPage extends React.Component {
renderMethodChoiceBox() {
const application = this.getApplicationObj();
const items = [
{label: i18next.t("general:Password"), key: "password"},
];
application.enableCodeSignin ? items.push({
label: i18next.t("login:Verification code"),
key: "verificationCode",
}) : null;
const items = [];
items.push({label: i18next.t("general:Password"), key: "password"});
application.enableCodeSignin ? items.push({label: i18next.t("login:Verification code"), key: "verificationCode"}) : null;
application.enableWebAuthn ? items.push({label: i18next.t("login:WebAuthn"), key: "webAuthn"}) : null;
if (application.enableCodeSignin || application.enableWebAuthn) {
@ -815,12 +847,9 @@ class LoginPage extends React.Component {
Setting.renderLogo(application)
}
<LanguageSelect languages={application.organizationObj.languages} style={{top: "55px", right: "5px", position: "absolute"}} />
{
this.renderSignedInBox()
}
{
this.renderForm(application)
}
{this.state.getVerifyTotp !== undefined ? null : this.renderSignedInBox()}
{this.state.getVerifyTotp !== undefined ? null : this.renderForm(application)}
{this.state.getVerifyTotp !== undefined ? this.state.getVerifyTotp() : null}
</div>
</div>
</div>

View File

@ -0,0 +1,120 @@
// 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, {useState} from "react";
import i18next from "i18next";
import {Button, Input} from "antd";
import * as AuthBackend from "./AuthBackend";
import {SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm} from "./MfaVerifyForm";
export const NextMfa = "NextMfa";
export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, application, onSuccess, onFail}) {
formValues.password = "";
formValues.username = "";
const [loading, setLoading] = useState(false);
const [type, setType] = useState(mfaProps.type);
const [recoveryCode, setRecoveryCode] = useState("");
const verify = ({passcode}) => {
setLoading(true);
const values = {...formValues, passcode, mfaType: type};
AuthBackend.login(values, oAuthParams).then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res.msg);
}
}).catch((reason) => {
onFail(reason.message);
}).finally(() => {
setLoading(false);
});
};
const recover = () => {
setLoading(true);
AuthBackend.login({...formValues, recoveryCode}, oAuthParams).then(res => {
if (res.status === "ok") {
onSuccess();
} else {
onFail(res.msg);
}
}).catch((reason) => {
onFail(reason.message);
}).finally(() => {
setLoading(false);
});
};
switch (type) {
case SmsMfaType:
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
{i18next.t("mfa:Multi-factor authentication")}
</div>
<div style={{marginBottom: 24}}>
{i18next.t("mfa:Multi-factor authentication description")}
</div>
<MfaSmsVerifyForm
mfaProps={mfaProps}
onFinish={verify}
application={application}
/>
<span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")}
<a onClick={() => {
setType("recovery");
}}>
{i18next.t("mfa:Use a recovery code")}
</a>
</span>
</div>
);
case "recovery":
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
{i18next.t("mfa:Multi-factor recover")}
</div>
<div style={{marginBottom: 24}}>
{i18next.t("mfa:Multi-factor recover description")}
</div>
<Input placeholder={i18next.t("mfa:Recovery code")}
style={{marginBottom: 24}}
type={"passcode"}
size={"large"}
onChange={event => setRecoveryCode(event.target.value)}
/>
<Button style={{width: "100%", marginBottom: 20}} size={"large"} loading={loading}
type={"primary"} onClick={() => {
recover();
}}>{i18next.t("forget:Verify")}
</Button>
<span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")}
<a onClick={() => {
setType(mfaProps.type);
}}>
{i18next.t("mfa:Use SMS verification code")}
</a>
</span>
</div>
);
default:
return null;
}
}

View File

@ -0,0 +1,275 @@
// 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, {useState} from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as MfaBackend from "../backend/MfaBackend";
import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserBackend from "../backend/UserBackend";
import {MfaSmsVerifyForm, MfaTotpVerifyForm} from "./MfaVerifyForm";
import * as ApplicationBackend from "../backend/ApplicationBackend";
const {Step} = Steps;
export const SmsMfaType = "sms";
export const TotpMfaType = "app";
function CheckPasswordForm({user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({password}) => {
const data = {...user, password};
UserBackend.checkUserPassword(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.finally(() => {
form.setFieldsValue({password: ""});
});
};
return (
<Form
form={form}
style={{width: "300px", marginTop: "20px"}}
onFinish={onFinish}
>
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
}
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, type: mfaProps.type, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
})
.finally(() => {
form.setFieldsValue({passcode: ""});
});
};
if (mfaProps.type === SmsMfaType) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} />;
} else if (mfaProps.type === TotpMfaType) {
return <MfaTotpVerifyForm onFinish={onFinish} mfaProps={mfaProps} />;
} else {
return <div></div>;
}
}
function EnableMfaForm({user, mfaProps, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableTotp = () => {
const data = {
type: mfaProps.type,
...user,
};
setLoading(true);
MfaBackend.MfaSetupEnable(data).then(res => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
}
).finally(() => {
setLoading(false);
});
};
return (
<div style={{width: "400px"}}>
<p>{i18next.t("mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code")}</p>
<br />
<code style={{fontStyle: "solid"}}>{mfaProps.recoveryCodes[0]}</code>
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableTotp();
}} block type="primary">
{i18next.t("general:Enable")}
</Button>
</div>
);
}
class MfaSetupPage extends React.Component {
constructor(props) {
super(props);
this.state = {
account: props.account,
current: 0,
type: props.type ?? SmsMfaType,
mfaProps: null,
};
}
componentDidMount() {
this.getApplication();
}
getApplication() {
ApplicationBackend.getApplication("admin", this.state.account.signupApplication)
.then((application) => {
if (application !== null) {
this.setState({
application: application,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to get application"));
}
});
}
getUser() {
return {
name: this.state.account.name,
owner: this.state.account.owner,
};
}
renderStep() {
switch (this.state.current) {
case 0:
return <CheckPasswordForm
user={this.getUser()}
onSuccess={() => {
MfaBackend.MfaSetupInitiate({
type: this.state.type,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
this.setState({
current: this.state.current + 1,
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}}
/>;
case 1:
return <MfaVerifyForm
mfaProps={{...this.state.mfaProps}}
application={this.state.application}
user={this.getUser()}
onSuccess={() => {
this.setState({
current: this.state.current + 1,
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("general:Failed to verify"));
}}
/>;
case 2:
return <EnableMfaForm user={this.getUser()} mfaProps={{type: this.state.type, ...this.state.mfaProps}}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
Setting.goToLinkSoft(this, "/account");
}}
onFail={(res) => {
Setting.showMessage("error", `${i18next.t("general:Failed to enable")}: ${res.msg}`);
}} />;
default:
return null;
}
}
render() {
if (!this.props.account) {
return (
<Result
status="403"
title="403 Unauthorized"
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
/>
);
}
return (
<Row>
<Col span={24} style={{justifyContent: "center"}}>
<Row>
<Col span={24}>
<div style={{textAlign: "center", fontSize: "28px"}}>
{i18next.t("mfa:Protect your account with Multi-factor authentication")}</div>
<div style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</div>
</Col>
</Row>
<Row>
<Col span={24}>
<Steps current={this.state.current} style={{
width: "90%",
maxWidth: "500px",
margin: "auto",
marginTop: "80px",
}} >
<Step title={i18next.t("mfa:Verify Password")} icon={<UserOutlined />} />
<Step title={i18next.t("mfa:Verify Code")} icon={<KeyOutlined />} />
<Step title={i18next.t("general:Enable")} icon={<CheckOutlined />} />
</Steps>
</Col>
</Row>
</Col>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "10px", textAlign: "center"}}>{this.renderStep()}</div>
</Col>
</Row>
);
}
}
export default MfaSetupPage;

View File

@ -0,0 +1,163 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Form, Input, Row} from "antd";
import i18next from "i18next";
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {SendCodeInput} from "../common/SendCodeInput";
import * as Setting from "../Setting";
import React from "react";
import QRCode from "qrcode.react";
import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
const [dest, setDest] = React.useState(mfaProps?.secret ?? "");
const [form] = Form.useForm();
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
{mfaProps?.secret !== undefined ?
<div style={{marginBottom: 20}}>
{Setting.IsEmail(dest) ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{Setting.IsEmail(dest) ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: Setting.IsEmail(dest) ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={i18next.t("general:Phone or email")}
/>
</Form.Item>
</Input.Group>
}
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<SendCodeInput
countryCode={form.getFieldValue("countryCode")}
method={mfaProps?.id === undefined ? "mfaSetup" : "mfaAuth"}
onButtonClickArgs={[dest, Setting.IsEmail(dest) ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};
export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm();
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
<Row type="flex" justify="center" align="middle">
<Col>
<QRCode value={mfaProps.url} size={200} />
</Col>
</Row>
<Row type="flex" justify="center" align="middle">
<Col>
{Setting.getLabel(
i18next.t("mfa:Multi-factor secret"),
i18next.t("mfa:Multi-factor secret - Tooltip")
)}
:
</Col>
<Col>
<Input value={mfaProps.secret} />
</Col>
<Col>
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Col>
</Row>
<Form.Item
name="passcode"
rules={[{required: true, message: "Please input your passcode"}]}
>
<Input
style={{marginTop: 24}}
prefix={<UserOutlined />}
placeholder={i18next.t("mfa:Passcode")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Result} from "antd";
import {Button, Card, Result, Spin} from "antd";
import i18next from "i18next";
import {authConfig} from "./Auth";
import * as ApplicationBackend from "../backend/ApplicationBackend";
@ -44,37 +44,60 @@ class ResultPage extends React.Component {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
this.setState({
application: application,
});
});
}
onUpdateApplication(application) {
this.props.onUpdateApplication(application);
}
render() {
const application = this.state.application;
if (application === null) {
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "10%"}} />
</div>
);
}
return (
<div>
{
Setting.renderHelmet(application)
}
<Result
status="success"
title={i18next.t("signup:Your account has been created!")}
subTitle={i18next.t("signup:Please click the below button to sign in")}
extra={[
<Button type="primary" key="login" onClick={() => {
const linkInStorage = sessionStorage.getItem("signinUrl");
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLink(linkInStorage);
} else {
Setting.redirectToLoginPage(application, this.props.history);
}
}}>
{i18next.t("login:Sign In")}
</Button>,
]}
/>
<div style={{display: "flex", flex: "1", justifyContent: "center"}}>
<Card>
<div style={{marginTop: "30px", marginBottom: "30px", textAlign: "center"}}>
{
Setting.renderHelmet(application)
}
{
Setting.renderLogo(application)
}
{
Setting.renderHelmet(application)
}
<Result
status="success"
title={i18next.t("signup:Your account has been created!")}
subTitle={i18next.t("signup:Please click the below button to sign in")}
extra={[
<Button type="primary" key="login" onClick={() => {
const linkInStorage = sessionStorage.getItem("signinUrl");
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLink(linkInStorage);
} else {
Setting.redirectToLoginPage(application, this.props.history);
}
}}>
{i18next.t("login:Sign In")}
</Button>,
]}
/>
</div>
</Card>
</div>
);
}

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