mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-14 08:03:23 +08:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
eb39e9e044 | |||
5b27f939b8 | |||
69ee6a6f7e | |||
bf6d5e529b | |||
55fd31f575 | |||
05c063ac24 | |||
38da63e73c | |||
cb13d693e6 | |||
d699774179 | |||
84a7fdcd07 | |||
2cd6f9df8e | |||
eea2e1d271 | |||
48c5bd942c | |||
d01d63d82a | |||
e4fd9cca92 | |||
8d531b8880 | |||
b1589e11eb | |||
b32a772a77 | |||
7e4562efe1 | |||
3a6ab4cfc6 |
135
ai/ai.go
Normal file
135
ai/ai.go
Normal 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
42
ai/ai_test.go
Normal 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
28
ai/proxy.go
Normal 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
28
ai/util.go
Normal 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
|
||||
}
|
@ -24,7 +24,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
@ -70,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)
|
||||
@ -133,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 {
|
||||
@ -298,7 +299,6 @@ func (c *ApiController) Login() {
|
||||
|
||||
password := authForm.Password
|
||||
user, msg = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
|
||||
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
@ -458,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,
|
||||
@ -519,6 +522,38 @@ 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
|
||||
@ -536,7 +571,7 @@ func (c *ApiController) Login() {
|
||||
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), authForm = %s"), util.StructToJson(authForm)))
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), form = %s"), util.StructToJson(authForm)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -168,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"}
|
||||
|
@ -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
190
controllers/mfa.go
Normal 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)
|
||||
}
|
@ -158,6 +158,11 @@ 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
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -27,10 +27,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SignupVerification = "signup"
|
||||
ResetVerification = "reset"
|
||||
LoginVerification = "login"
|
||||
ForgetVerification = "forget"
|
||||
SignupVerification = "signup"
|
||||
ResetVerification = "reset"
|
||||
LoginVerification = "login"
|
||||
ForgetVerification = "forget"
|
||||
MfaSetupVerification = "mfaSetup"
|
||||
MfaAuthVerification = "mfaAuth"
|
||||
)
|
||||
|
||||
// SendVerificationCode ...
|
||||
@ -78,6 +80,11 @@ func (c *ApiController) SendVerificationCode() {
|
||||
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 vform.Type {
|
||||
@ -99,6 +106,11 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
} 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()
|
||||
@ -119,6 +131,13 @@ func (c *ApiController) SendVerificationCode() {
|
||||
if user = c.getCurrentUser(); user != nil {
|
||||
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()
|
||||
@ -130,6 +149,11 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
}
|
||||
|
||||
if vform.Method == MfaSetupVerification {
|
||||
c.SetSession(object.MfaSmsCountryCodeSession, vform.CountryCode)
|
||||
c.SetSession(object.MfaSmsDestSession, vform.Dest)
|
||||
}
|
||||
|
||||
if sendResp != nil {
|
||||
c.ResponseError(sendResp.Error())
|
||||
} else {
|
||||
|
@ -50,4 +50,8 @@ type AuthForm struct {
|
||||
CaptchaType string `json:"captchaType"`
|
||||
CaptchaToken string `json:"captchaToken"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
MfaType string `json:"mfaType"`
|
||||
Passcode string `json:"passcode"`
|
||||
RecoveryCode string `json:"recoveryCode"`
|
||||
}
|
||||
|
5
go.mod
5
go.mod
@ -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,17 +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
10
go.sum
@ -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=
|
||||
|
@ -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",
|
||||
@ -130,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"
|
||||
|
@ -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",
|
||||
@ -130,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"
|
||||
|
@ -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",
|
||||
@ -130,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"
|
||||
|
@ -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",
|
||||
@ -130,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"
|
||||
|
@ -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",
|
||||
@ -130,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"
|
||||
|
@ -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": "表示名は空白にできません",
|
||||
@ -130,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": "ユーザーは存在しません。まず登録してください"
|
||||
|
@ -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는 비어 있을 수 없습니다",
|
||||
@ -130,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": "사용자가 존재하지 않습니다. 먼저 회원 가입 해주세요"
|
||||
|
@ -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": "Имя отображения не может быть пустым",
|
||||
@ -130,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": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"
|
||||
|
@ -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",
|
||||
@ -130,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"
|
||||
|
@ -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": "显示名称不可为空",
|
||||
@ -130,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": "用户不存在,请先注册"
|
||||
|
16
i18n/util.go
16
i18n/util.go
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ type UserInfo struct {
|
||||
DisplayName string
|
||||
UnionId string
|
||||
Email string
|
||||
Phone string
|
||||
CountryCode string
|
||||
AvatarUrl string
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"},
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
107
object/mfa.go
Normal file
107
object/mfa.go
Normal 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
120
object/mfa_sms.go
Normal 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,
|
||||
}
|
||||
}
|
@ -49,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"`
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
@ -733,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]
|
||||
}
|
||||
}
|
||||
|
@ -265,6 +265,13 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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,12 @@ 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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
14
util/time.go
14
util/time.go
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Avatar, Input, List, Spin} from "antd";
|
||||
import {Alert, Avatar, Input, List, Spin} from "antd";
|
||||
import {CopyOutlined, DislikeOutlined, LikeOutlined, SendOutlined} from "@ant-design/icons";
|
||||
import i18next from "i18next";
|
||||
|
||||
@ -30,7 +30,7 @@ class ChatBox extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.messages !== this.props.messages && this.props.messages !== null) {
|
||||
if (prevProps.messages !== this.props.messages && this.props.messages !== undefined && this.props.messages !== null) {
|
||||
this.scrollToListItem(this.props.messages.length);
|
||||
}
|
||||
}
|
||||
@ -74,6 +74,16 @@ 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 (
|
||||
@ -107,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"}}
|
||||
>
|
||||
|
@ -110,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);
|
||||
@ -126,7 +136,7 @@ class ChatMenu extends React.Component {
|
||||
const items = this.chatsToItems(this.props.chats);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
style={{
|
||||
@ -152,6 +162,7 @@ class ChatMenu extends React.Component {
|
||||
New Chat
|
||||
</Button>
|
||||
<Menu
|
||||
style={{maxHeight: "calc(100vh - 140px - 40px - 8px)", overflowY: "auto"}}
|
||||
mode="inline"
|
||||
openKeys={this.state.openKeys}
|
||||
selectedKeys={this.state.selectedKeys}
|
||||
@ -159,7 +170,7 @@ class ChatMenu extends React.Component {
|
||||
onSelect={this.onSelect}
|
||||
items={items}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,12 @@ import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class ChatPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.menu = React.createRef();
|
||||
}
|
||||
|
||||
newChat(chat) {
|
||||
const randomName = Setting.getRandomName();
|
||||
return {
|
||||
@ -50,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,
|
||||
};
|
||||
@ -77,6 +84,35 @@ 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}`);
|
||||
});
|
||||
}
|
||||
@ -94,7 +130,7 @@ class ChatPage extends BaseListPage {
|
||||
this.getMessages(newChat.name);
|
||||
|
||||
const {pagination} = this.state;
|
||||
this.fetch({pagination});
|
||||
this.fetch({pagination}, false);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
@ -114,7 +150,7 @@ class ChatPage extends BaseListPage {
|
||||
if (j < 0) {
|
||||
this.setState({
|
||||
chatName: undefined,
|
||||
messages: undefined,
|
||||
messages: [],
|
||||
data: data,
|
||||
});
|
||||
} else {
|
||||
@ -135,6 +171,10 @@ class ChatPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentChat() {
|
||||
return this.state.data.filter(chat => chat.name === this.state.chatName)[0];
|
||||
}
|
||||
|
||||
renderTable(chats) {
|
||||
const onSelectChat = (i) => {
|
||||
const chat = chats[i];
|
||||
@ -146,7 +186,7 @@ class ChatPage extends BaseListPage {
|
||||
};
|
||||
|
||||
const onAddChat = () => {
|
||||
const chat = this.state.data.filter(chat => chat.name === this.state.chatName)[0];
|
||||
const chat = this.getCurrentChat();
|
||||
this.addChat(chat);
|
||||
};
|
||||
|
||||
@ -165,12 +205,12 @@ class ChatPage extends BaseListPage {
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", height: "calc(100vh - 140px)"}}>
|
||||
<div style={{width: "250px", height: "100%", backgroundColor: "white", borderRight: "1px solid rgb(245,245,245)"}}>
|
||||
<ChatMenu chats={chats} onSelectChat={onSelectChat} onAddChat={onAddChat} onDeleteChat={onDeleteChat} />
|
||||
<div 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 === null ? null : (
|
||||
(this.state.messages === undefined || this.state.messages === null) ? null : (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
top: -50,
|
||||
@ -184,6 +224,7 @@ class ChatPage extends BaseListPage {
|
||||
backgroundBlendMode: "luminosity",
|
||||
filter: "grayscale(80%) brightness(140%) contrast(90%)",
|
||||
opacity: 0.5,
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
</div>
|
||||
)
|
||||
@ -194,7 +235,7 @@ class ChatPage extends BaseListPage {
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -204,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,
|
||||
@ -227,6 +271,10 @@ class ChatPage extends BaseListPage {
|
||||
chatName: chat.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (!setLoading) {
|
||||
this.menu.current.setSelectedKeyToNewChat(chats);
|
||||
}
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
|
@ -24,6 +24,7 @@ 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";
|
||||
|
||||
@ -54,10 +55,6 @@ class EntryPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getApplicationObj() {
|
||||
return this.state.application || null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const onUpdateApplication = (application) => {
|
||||
this.setState({
|
||||
@ -84,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>
|
||||
|
@ -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: "",
|
||||
};
|
||||
|
@ -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"}} >
|
||||
|
@ -22,6 +22,21 @@ import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
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() {
|
||||
const randomName = Setting.getRandomName();
|
||||
@ -30,7 +45,7 @@ class ModelListPage extends BaseListPage {
|
||||
name: `model_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Model - ${randomName}`,
|
||||
modelText: "",
|
||||
modelText: rbacModel,
|
||||
isEnabled: true,
|
||||
};
|
||||
}
|
||||
|
@ -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") {
|
||||
|
@ -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"},
|
||||
|
@ -132,6 +132,34 @@ class ProviderEditPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
@ -446,7 +474,7 @@ class ProviderEditPage extends React.Component {
|
||||
this.state.provider.category === "AI" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{this.getClientIdLabel(this.state.provider)}
|
||||
{this.getClientIdLabel(this.state.provider)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.clientId} onChange={e => {
|
||||
@ -458,7 +486,7 @@ class ProviderEditPage extends React.Component {
|
||||
}
|
||||
<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 => {
|
||||
@ -470,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 => {
|
||||
@ -490,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 => {
|
||||
|
@ -1057,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;
|
||||
}
|
||||
|
||||
|
@ -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"}} >
|
||||
|
@ -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={() => {
|
||||
|
@ -48,7 +48,7 @@ export function getEmailAndPhone(organization, username) {
|
||||
|
||||
export function oAuthParamsToQuery(oAuthParams) {
|
||||
// login
|
||||
if (oAuthParams === null) {
|
||||
if (oAuthParams === null || oAuthParams === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ import LanguageSelect from "../common/select/LanguageSelect";
|
||||
import {CaptchaModal} from "../common/modal/CaptchaModal";
|
||||
import {CaptchaRule} from "../common/modal/CaptchaModal";
|
||||
import RedirectForm from "../common/RedirectForm";
|
||||
import {MfaAuthVerifyForm, NextMfa} from "./MfaAuthVerifyForm";
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props) {
|
||||
@ -323,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") {
|
||||
@ -350,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}`);
|
||||
}
|
||||
@ -827,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>
|
||||
|
120
web/src/auth/MfaAuthVerifyForm.js
Normal file
120
web/src/auth/MfaAuthVerifyForm.js
Normal 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;
|
||||
}
|
||||
}
|
275
web/src/auth/MfaSetupPage.js
Normal file
275
web/src/auth/MfaSetupPage.js
Normal 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;
|
163
web/src/auth/MfaVerifyForm.js
Normal file
163
web/src/auth/MfaVerifyForm.js
Normal 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>
|
||||
);
|
||||
};
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Result, Spin} from "antd";
|
||||
import {Button, Card, Result, Spin} from "antd";
|
||||
import i18next from "i18next";
|
||||
import {authConfig} from "./Auth";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
@ -44,12 +44,17 @@ 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;
|
||||
|
||||
@ -62,27 +67,37 @@ class ResultPage extends React.Component {
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ export function getOAuthGetParameters(params) {
|
||||
|
||||
export function getStateFromQueryParams(applicationName, providerName, method, isShortState) {
|
||||
let query = window.location.search;
|
||||
query = `${query}&application=${applicationName}&provider=${providerName}&method=${method}`;
|
||||
query = `${query}&application=${encodeURIComponent(applicationName)}&provider=${encodeURIComponent(providerName)}&method=${method}`;
|
||||
if (method === "link") {
|
||||
query = `${query}&from=${window.location.pathname}`;
|
||||
}
|
||||
|
@ -44,6 +44,23 @@ export function getMessage(owner, name) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getMessageAnswer(owner, name, onMessage, onError) {
|
||||
const eventSource = new EventSource(`${Setting.ServerUrl}/api/get-message-answer?id=${owner}/${encodeURIComponent(name)}`);
|
||||
|
||||
eventSource.addEventListener("message", (e) => {
|
||||
onMessage(e.data);
|
||||
});
|
||||
|
||||
eventSource.addEventListener("myerror", (e) => {
|
||||
onError(e.data);
|
||||
eventSource.close();
|
||||
});
|
||||
|
||||
eventSource.addEventListener("end", (e) => {
|
||||
eventSource.close();
|
||||
});
|
||||
}
|
||||
|
||||
export function updateMessage(owner, name, message) {
|
||||
const newMessage = Setting.deepCopy(message);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-message?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
|
76
web/src/backend/MfaBackend.js
Normal file
76
web/src/backend/MfaBackend.js
Normal file
@ -0,0 +1,76 @@
|
||||
// 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 * as Setting from "../Setting";
|
||||
|
||||
export function MfaSetupInitiate(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
formData.append("type", values.type);
|
||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/initiate`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function MfaSetupVerify(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
formData.append("type", values.type);
|
||||
formData.append("passcode", values.passcode);
|
||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function MfaSetupEnable(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("type", values.type);
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function DeleteMfa(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("id", values.id);
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-mfa`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function SetPreferredMfa(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("id", values.id);
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
return fetch(`${Setting.ServerUrl}/api/set-preferred-mfa`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
}).then((res) => res.json());
|
||||
}
|
@ -203,3 +203,11 @@ export function verifyCode(values) {
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function checkUserPassword(values) {
|
||||
return fetch(`${Setting.ServerUrl}/api/check-user-password`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(values),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
@ -21,10 +21,8 @@ import {CaptchaModal} from "./modal/CaptchaModal";
|
||||
|
||||
const {Search} = Input;
|
||||
|
||||
export const SendCodeInput = (props) => {
|
||||
const {disabled, textBefore, onChange, onButtonClickArgs, application, method, countryCode} = props;
|
||||
export const SendCodeInput = ({value, disabled, textBefore, onChange, onButtonClickArgs, application, method, countryCode}) => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
const [buttonLeftTime, setButtonLeftTime] = React.useState(0);
|
||||
const [buttonLoading, setButtonLoading] = React.useState(false);
|
||||
|
||||
@ -62,6 +60,7 @@ export const SendCodeInput = (props) => {
|
||||
<Search
|
||||
addonBefore={textBefore}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
prefix={<SafetyOutlined />}
|
||||
placeholder={i18next.t("code:Enter your code")}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
|
@ -29,8 +29,6 @@ export const THEMES = {
|
||||
comic: `${Setting.StaticBaseUrl}/img/theme_comic.svg`,
|
||||
};
|
||||
|
||||
Object.values(THEMES).map(value => new Image().src = value);
|
||||
|
||||
const themeTypes = {
|
||||
default: "Default", // i18next.t("theme:Default")
|
||||
dark: "Dark", // i18next.t("theme:Dark")
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "Bearbeiten",
|
||||
"Email": "E-Mail",
|
||||
"Email - Tooltip": "Gültige E-Mail-Adresse",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "Fehler beim hinzufügen",
|
||||
"Failed to connect to server": "Die Verbindung zum Server konnte nicht hergestellt werden",
|
||||
"Failed to delete": "Konnte nicht gelöscht werden",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "Konnte nicht gespeichert werden",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "Favicon-URL, die auf allen Casdoor-Seiten der Organisation verwendet wird",
|
||||
"First name": "Vorname",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Berechtigungen, die diesem Benutzer gehören",
|
||||
"Phone": "Telefon",
|
||||
"Phone - Tooltip": "Telefonnummer",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Vorschau",
|
||||
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
|
||||
"Products": "Produkte",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Modell bearbeiten",
|
||||
"Model text": "Modelltext",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "Ob das Scannen von QR-Codes zum Einloggen aktiviert werden soll",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Endpoint (Intranet)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "Host",
|
||||
"Host - Tooltip": "Name des Hosts",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "Stadt des Wohnsitzes",
|
||||
"Managed accounts": "Verwaltete Konten",
|
||||
"Modify password...": "Passwort ändern...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "Neue E-Mail",
|
||||
"New Password": "Neues Passwort",
|
||||
"New User": "Neuer Benutzer",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "Edit",
|
||||
"Email": "Email",
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "Failed to add",
|
||||
"Failed to connect to server": "Failed to connect to server",
|
||||
"Failed to delete": "Failed to delete",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "Failed to save",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
|
||||
"First name": "First name",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Permissions owned by this user",
|
||||
"Phone": "Phone",
|
||||
"Phone - Tooltip": "Phone number",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Preview",
|
||||
"Preview - Tooltip": "Preview the configured effects",
|
||||
"Products": "Products",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Setup Multi-factor authentication",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "If you are unable to access your device, enter your recovery code to verify your identity",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Edit Model",
|
||||
"Model text": "Model text",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "Whether to allow scanning QR code to login",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Endpoint (Intranet)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "Email address of \"From\"",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "Name of \"From\"",
|
||||
"Host": "Host",
|
||||
"Host - Tooltip": "Name of host",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "City of residence",
|
||||
"Managed accounts": "Managed accounts",
|
||||
"Modify password...": "Modify password...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "New Email",
|
||||
"New Password": "New Password",
|
||||
"New User": "New User",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "Editar",
|
||||
"Email": "Correo electrónico",
|
||||
"Email - Tooltip": "Dirección de correo electrónico válida",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "No se pudo agregar",
|
||||
"Failed to connect to server": "No se pudo conectar al servidor",
|
||||
"Failed to delete": "No se pudo eliminar",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "No se pudo guardar",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "URL del icono Favicon utilizado en todas las páginas de Casdoor de la organización",
|
||||
"First name": "Nombre de pila",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Permisos propiedad de este usuario",
|
||||
"Phone": "Teléfono",
|
||||
"Phone - Tooltip": "Número de teléfono",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Avance",
|
||||
"Preview - Tooltip": "Vista previa de los efectos configurados",
|
||||
"Products": "Productos",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Editar modelo",
|
||||
"Model text": "Texto modelo",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "Si permitir el escaneo de códigos QR para acceder",
|
||||
"Endpoint": "Punto final",
|
||||
"Endpoint (Intranet)": "Punto final (intranet)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "Anfitrión",
|
||||
"Host - Tooltip": "Nombre del anfitrión",
|
||||
"IdP": "IdP = Proveedor de Identidad",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "Ciudad de residencia",
|
||||
"Managed accounts": "Cuentas gestionadas",
|
||||
"Modify password...": "Modificar contraseña...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "Nuevo correo electrónico",
|
||||
"New Password": "Nueva contraseña",
|
||||
"New User": "Nuevo Usuario",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "Modifier",
|
||||
"Email": "Email",
|
||||
"Email - Tooltip": "Adresse e-mail valide",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "Échec d'ajout",
|
||||
"Failed to connect to server": "Échec de la connexion au serveur",
|
||||
"Failed to delete": "Échec de la suppression",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "Échec de sauvegarde",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "L'URL de l'icône Favicon utilisée dans toutes les pages Casdoor de l'organisation",
|
||||
"First name": "Prénom",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Autorisations détenues par cet utilisateur",
|
||||
"Phone": "Téléphone",
|
||||
"Phone - Tooltip": "Numéro de téléphone",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Aperçu",
|
||||
"Preview - Tooltip": "Prévisualisez les effets configurés",
|
||||
"Products": "Produits",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Modifier le modèle",
|
||||
"Model text": "Texte modèle",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "Doit-on autoriser la numérisation de QR code pour se connecter ?",
|
||||
"Endpoint": "Point final",
|
||||
"Endpoint (Intranet)": "Point final (intranet)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "Hôte",
|
||||
"Host - Tooltip": "Nom d'hôte",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "Ville de résidence",
|
||||
"Managed accounts": "Comptes gérés",
|
||||
"Modify password...": "Modifier le mot de passe...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "Nouveau courrier électronique",
|
||||
"New Password": "Nouveau mot de passe",
|
||||
"New User": "Nouvel utilisateur",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "Mengedit",
|
||||
"Email": "Email",
|
||||
"Email - Tooltip": "Alamat email yang valid",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "Gagal menambahkan",
|
||||
"Failed to connect to server": "Gagal terhubung ke server",
|
||||
"Failed to delete": "Gagal menghapus",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "Gagal menyimpan",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "URL ikon Favicon yang digunakan di semua halaman Casdoor organisasi",
|
||||
"First name": "Nama depan",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Izin dimiliki oleh pengguna ini",
|
||||
"Phone": "Telepon",
|
||||
"Phone - Tooltip": "Nomor telepon",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Tinjauan",
|
||||
"Preview - Tooltip": "Mengawali pratinjau efek yang sudah dikonfigurasi",
|
||||
"Products": "Produk",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Mengedit Model",
|
||||
"Model text": "Teks Model",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "Apakah diizinkan untuk memindai kode QR untuk masuk?",
|
||||
"Endpoint": "Titik akhir",
|
||||
"Endpoint (Intranet)": "Titik Akhir (Intranet)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "Tuan rumah",
|
||||
"Host - Tooltip": "Nama tuan rumah",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "Kota tempat tinggal",
|
||||
"Managed accounts": "Akun yang dikelola",
|
||||
"Modify password...": "Mengubah kata sandi...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "Email baru",
|
||||
"New Password": "Kata Sandi Baru",
|
||||
"New User": "Pengguna Baru",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "編集",
|
||||
"Email": "電子メール",
|
||||
"Email - Tooltip": "有効な電子メールアドレス",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "追加できませんでした",
|
||||
"Failed to connect to server": "サーバーに接続できませんでした",
|
||||
"Failed to delete": "削除に失敗しました",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "保存に失敗しました",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "ファビコン",
|
||||
"Favicon - Tooltip": "組織のすべてのCasdoorページに使用されるFaviconアイコンのURL",
|
||||
"First name": "名前",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "このユーザーが所有する権限",
|
||||
"Phone": "電話",
|
||||
"Phone - Tooltip": "電話番号",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "プレビュー",
|
||||
"Preview - Tooltip": "構成されたエフェクトをプレビューする",
|
||||
"Products": "製品",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "編集モデル",
|
||||
"Model text": "モデルテキスト",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "ログインするためにQRコードをスキャンすることを許可するかどうか",
|
||||
"Endpoint": "エンドポイント",
|
||||
"Endpoint (Intranet)": "エンドポイント(イントラネット)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "ホスト",
|
||||
"Host - Tooltip": "ホストの名前",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "居住都市",
|
||||
"Managed accounts": "管理アカウント",
|
||||
"Modify password...": "パスワードを変更する...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "新しいメール",
|
||||
"New Password": "新しいパスワード",
|
||||
"New User": "新しいユーザー",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "편집",
|
||||
"Email": "이메일",
|
||||
"Email - Tooltip": "유효한 이메일 주소",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "추가하지 못했습니다",
|
||||
"Failed to connect to server": "서버에 연결하지 못했습니다",
|
||||
"Failed to delete": "삭제에 실패했습니다",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "저장에 실패했습니다",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "파비콘",
|
||||
"Favicon - Tooltip": "조직의 모든 Casdoor 페이지에서 사용되는 Favicon 아이콘 URL",
|
||||
"First name": "이름",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "이 사용자가 소유한 권한",
|
||||
"Phone": "전화기",
|
||||
"Phone - Tooltip": "전화 번호",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "미리보기",
|
||||
"Preview - Tooltip": "구성된 효과를 미리보기합니다",
|
||||
"Products": "제품들",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "편집 형태 모델",
|
||||
"Model text": "모델 텍스트",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "QR 코드를 스캔해서 로그인할 수 있는지 여부",
|
||||
"Endpoint": "엔드포인트",
|
||||
"Endpoint (Intranet)": "엔드포인트 (Intranet)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "호스트",
|
||||
"Host - Tooltip": "호스트의 이름",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "거주 도시",
|
||||
"Managed accounts": "관리 계정",
|
||||
"Modify password...": "비밀번호 수정하기...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "새 이메일",
|
||||
"New Password": "새로운 비밀번호",
|
||||
"New User": "새로운 사용자",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "Редактировать",
|
||||
"Email": "Электронная почта",
|
||||
"Email - Tooltip": "Действительный адрес электронной почты",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "Не удалось добавить",
|
||||
"Failed to connect to server": "Не удалось подключиться к серверу",
|
||||
"Failed to delete": "Не удалось удалить",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "Не удалось сохранить",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "Фавикон",
|
||||
"Favicon - Tooltip": "URL иконки Favicon, используемый на всех страницах организации Casdoor",
|
||||
"First name": "Имя",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Разрешения, принадлежащие этому пользователю",
|
||||
"Phone": "Телефон",
|
||||
"Phone - Tooltip": "Номер телефона",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Предварительный просмотр",
|
||||
"Preview - Tooltip": "Предварительный просмотр настроенных эффектов",
|
||||
"Products": "Продукты",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Редактировать модель",
|
||||
"Model text": "Модельный текст",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "Разрешить ли сканирование QR-кода для входа в систему",
|
||||
"Endpoint": "Конечная точка",
|
||||
"Endpoint (Intranet)": "Конечная точка (интранет)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "Хост",
|
||||
"Host - Tooltip": "Имя хоста",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "Город проживания",
|
||||
"Managed accounts": "Управляемые счета",
|
||||
"Modify password...": "Изменить пароль...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "Новое электронное письмо",
|
||||
"New Password": "Новый пароль",
|
||||
"New User": "Новый пользователь",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "Chỉnh sửa",
|
||||
"Email": "Email: Thư điện tử",
|
||||
"Email - Tooltip": "Địa chỉ email hợp lệ",
|
||||
"Enable": "Enable",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Failed to add": "Không thể thêm được",
|
||||
"Failed to connect to server": "Không thể kết nối đến máy chủ",
|
||||
"Failed to delete": "Không thể xoá",
|
||||
"Failed to enable": "Failed to enable",
|
||||
"Failed to get answer": "Failed to get answer",
|
||||
"Failed to save": "Không thể lưu được",
|
||||
"Failed to verify": "Failed to verify",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "URL biểu tượng Favicon được sử dụng trong tất cả các trang của tổ chức Casdoor",
|
||||
"First name": "Tên đầu tiên",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Quyền sở hữu của người dùng này",
|
||||
"Phone": "Điện thoại",
|
||||
"Phone - Tooltip": "Số điện thoại",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Xem trước",
|
||||
"Preview - Tooltip": "Xem trước các hiệu ứng đã cấu hình",
|
||||
"Products": "Sản phẩm",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "Text",
|
||||
"Text - Tooltip": "Text - Tooltip"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
|
||||
"Failed to get application": "Failed to get application",
|
||||
"Failed to initiate MFA": "Failed to initiate MFA",
|
||||
"Have problems?": "Have problems?",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
|
||||
"Multi-factor authentication description": "Multi-factor authentication description",
|
||||
"Multi-factor methods": "Multi-factor methods",
|
||||
"Multi-factor recover": "Multi-factor recover",
|
||||
"Multi-factor recover description": "Multi-factor recover description",
|
||||
"Multi-factor secret": "Multi-factor secret",
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
|
||||
"Passcode": "Passcode",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"Your email is": "Your email is",
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Chỉnh sửa mô hình",
|
||||
"Model text": "Văn bản mẫu",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "Cho phép quét mã QR để đăng nhập",
|
||||
"Endpoint": "Điểm cuối",
|
||||
"Endpoint (Intranet)": "Điểm kết thúc (mạng nội bộ)",
|
||||
"From address": "From address",
|
||||
"From address - Tooltip": "From address - Tooltip",
|
||||
"From name": "From name",
|
||||
"From name - Tooltip": "From name - Tooltip",
|
||||
"Host": "Chủ nhà",
|
||||
"Host - Tooltip": "Tên của người chủ chỗ ở",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "Thành phố cư trú",
|
||||
"Managed accounts": "Quản lý tài khoản",
|
||||
"Modify password...": "Sửa đổi mật khẩu...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "Email mới",
|
||||
"New Password": "Mật khẩu mới",
|
||||
"New User": "Người dùng mới",
|
||||
|
@ -193,10 +193,16 @@
|
||||
"Edit": "编辑",
|
||||
"Email": "电子邮箱",
|
||||
"Email - Tooltip": "合法的电子邮件地址",
|
||||
"Enable": "启用",
|
||||
"Enabled": "已开启",
|
||||
"Enabled successfully": "启用成功",
|
||||
"Failed to add": "添加失败",
|
||||
"Failed to connect to server": "连接服务器失败",
|
||||
"Failed to delete": "删除失败",
|
||||
"Failed to enable": "启用失败",
|
||||
"Failed to get answer": "获取回答失败",
|
||||
"Failed to save": "保存失败",
|
||||
"Failed to verify": "验证失败",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "该组织所有Casdoor页面中所使用的Favicon图标URL",
|
||||
"First name": "名字",
|
||||
@ -243,6 +249,7 @@
|
||||
"Permissions - Tooltip": "该用户所拥有的权限",
|
||||
"Phone": "手机号",
|
||||
"Phone - Tooltip": "手机号",
|
||||
"Phone or email": "手机或邮箱",
|
||||
"Preview": "预览",
|
||||
"Preview - Tooltip": "可预览所配置的效果",
|
||||
"Products": "商品",
|
||||
@ -366,6 +373,36 @@
|
||||
"Text": "内容",
|
||||
"Text - Tooltip": "消息的内容"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "每次登录帐户时,都需要密码和认证码",
|
||||
"Failed to get application": "获取应用失败",
|
||||
"Failed to initiate MFA": "初始化 MFA 失败",
|
||||
"Have problems?": "遇到问题?",
|
||||
"Multi-factor authentication": "多因素认证",
|
||||
"Multi-factor authentication - Tooltip ": "多因素认证 - Tooltip ",
|
||||
"Multi-factor authentication description": "您已经启用多因素认证,请输入认证码",
|
||||
"Multi-factor methods": "多因素认证方式",
|
||||
"Multi-factor recover": "重置多因素认证",
|
||||
"Multi-factor recover description": "如果您无法访问您的设备,输入您的多因素认证恢复代码来确认您的身份",
|
||||
"Multi-factor secret": "多因素密钥",
|
||||
"Multi-factor secret - Tooltip": "多因素密钥 - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板",
|
||||
"Passcode": "认证码",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "请保存此恢复代码。一旦您的设备无法提供身份验证码,您可以通过此恢复码重置多因素认证",
|
||||
"Protect your account with Multi-factor authentication": "通过多因素认证保护您的帐户",
|
||||
"Recovery code": "恢复码",
|
||||
"SMS/Email message": "短信或邮件认证",
|
||||
"Set preferred": "设为首选",
|
||||
"Setup": "设置",
|
||||
"Use SMS verification code": "使用手机或电子邮件发送验证码认证",
|
||||
"Use a recovery code": "使用恢复代码",
|
||||
"Verification failed": "验证失败",
|
||||
"Verify Code": "验证码",
|
||||
"Verify Password": "验证密码",
|
||||
"Your email is": "你的电子邮件",
|
||||
"Your phone is": "你的手机号",
|
||||
"preferred": "首选"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "编辑模型",
|
||||
"Model text": "模型文本",
|
||||
@ -544,6 +581,10 @@
|
||||
"Enable QR code - Tooltip": "是否允许扫描二维码登录",
|
||||
"Endpoint": "地域节点 (外网)",
|
||||
"Endpoint (Intranet)": "地域节点 (内网)",
|
||||
"From address": "发件人地址",
|
||||
"From address - Tooltip": "邮件里发件人的邮箱地址",
|
||||
"From name": "发件人名称",
|
||||
"From name - Tooltip": "邮件里发件人的显示名称",
|
||||
"Host": "主机",
|
||||
"Host - Tooltip": "主机名",
|
||||
"IdP": "IdP",
|
||||
@ -792,6 +833,7 @@
|
||||
"Location - Tooltip": "居住地址所在的城市",
|
||||
"Managed accounts": "托管账户",
|
||||
"Modify password...": "编辑密码...",
|
||||
"Multi-factor authentication": "多因素认证",
|
||||
"New Email": "新邮箱",
|
||||
"New Password": "新密码",
|
||||
"New User": "添加用户",
|
||||
|
@ -61,6 +61,51 @@ class AccountTable extends React.Component {
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
getItems = () => {
|
||||
return [
|
||||
{name: "Organization", label: i18next.t("general:Organization")},
|
||||
{name: "ID", label: i18next.t("general:ID")},
|
||||
{name: "Name", label: i18next.t("general:Name")},
|
||||
{name: "Display name", label: i18next.t("general:Display name")},
|
||||
{name: "Avatar", label: i18next.t("general:Avatar")},
|
||||
{name: "User type", label: i18next.t("general:User type")},
|
||||
{name: "Password", label: i18next.t("general:Password")},
|
||||
{name: "Email", label: i18next.t("general:Email")},
|
||||
{name: "Phone", label: i18next.t("general:Phone")},
|
||||
{name: "Country code", label: i18next.t("user:Country code")},
|
||||
{name: "Country/Region", label: i18next.t("user:Country/Region")},
|
||||
{name: "Location", label: i18next.t("user:Location")},
|
||||
{name: "Address", label: i18next.t("user:Address")},
|
||||
{name: "Affiliation", label: i18next.t("user:Affiliation")},
|
||||
{name: "Title", label: i18next.t("user:Title")},
|
||||
{name: "ID card type", label: i18next.t("user:ID card type")},
|
||||
{name: "ID card", label: i18next.t("user:ID card")},
|
||||
{name: "Homepage", label: i18next.t("user:Homepage")},
|
||||
{name: "Bio", label: i18next.t("user:Bio")},
|
||||
{name: "Tag", label: i18next.t("user:Tag")},
|
||||
{name: "Language", label: i18next.t("user:Language")},
|
||||
{name: "Gender", label: i18next.t("user:Gender")},
|
||||
{name: "Birthday", label: i18next.t("user:Birthday")},
|
||||
{name: "Education", label: i18next.t("user:Education")},
|
||||
{name: "Score", label: i18next.t("user:Score")},
|
||||
{name: "Karma", label: i18next.t("user:Karma")},
|
||||
{name: "Ranking", label: i18next.t("user:Ranking")},
|
||||
{name: "Signup application", label: i18next.t("general:Signup application")},
|
||||
{name: "Roles", label: i18next.t("general:Roles")},
|
||||
{name: "Permissions", label: i18next.t("general:Permissions")},
|
||||
{name: "3rd-party logins", label: i18next.t("user:3rd-party logins")},
|
||||
{name: "Properties", label: i18next.t("user:Properties")},
|
||||
{name: "Is online", label: i18next.t("user:Is online")},
|
||||
{name: "Is admin", label: i18next.t("user:Is admin")},
|
||||
{name: "Is global admin", label: i18next.t("user:Is global admin")},
|
||||
{name: "Is forbidden", label: i18next.t("user:Is forbidden")},
|
||||
{name: "Is deleted", label: i18next.t("user:Is deleted")},
|
||||
{name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")},
|
||||
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
|
||||
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
||||
];
|
||||
};
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
@ -68,65 +113,14 @@ class AccountTable extends React.Component {
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record, index) => {
|
||||
const items = [
|
||||
{name: "Organization", displayName: i18next.t("general:Organization")},
|
||||
{name: "ID", displayName: i18next.t("general:ID")},
|
||||
{name: "Name", displayName: i18next.t("general:Name")},
|
||||
{name: "Display name", displayName: i18next.t("general:Display name")},
|
||||
{name: "Avatar", displayName: i18next.t("general:Avatar")},
|
||||
{name: "User type", displayName: i18next.t("general:User type")},
|
||||
{name: "Password", displayName: i18next.t("general:Password")},
|
||||
{name: "Email", displayName: i18next.t("general:Email")},
|
||||
{name: "Phone", displayName: i18next.t("general:Phone")},
|
||||
{name: "Country code", displayName: i18next.t("user:Country code")},
|
||||
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
|
||||
{name: "Location", displayName: i18next.t("user:Location")},
|
||||
{name: "Address", displayName: i18next.t("user:Address")},
|
||||
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},
|
||||
{name: "Title", displayName: i18next.t("user:Title")},
|
||||
{name: "ID card type", displayName: i18next.t("user:ID card type")},
|
||||
{name: "ID card", displayName: i18next.t("user:ID card")},
|
||||
{name: "Homepage", displayName: i18next.t("user:Homepage")},
|
||||
{name: "Bio", displayName: i18next.t("user:Bio")},
|
||||
{name: "Tag", displayName: i18next.t("user:Tag")},
|
||||
{name: "Language", displayName: i18next.t("user:Language")},
|
||||
{name: "Gender", displayName: i18next.t("user:Gender")},
|
||||
{name: "Birthday", displayName: i18next.t("user:Birthday")},
|
||||
{name: "Education", displayName: i18next.t("user:Education")},
|
||||
{name: "Score", displayName: i18next.t("user:Score")},
|
||||
{name: "Karma", displayName: i18next.t("user:Karma")},
|
||||
{name: "Ranking", displayName: i18next.t("user:Ranking")},
|
||||
{name: "Signup application", displayName: i18next.t("general:Signup application")},
|
||||
{name: "Roles", displayName: i18next.t("general:Roles")},
|
||||
{name: "Permissions", displayName: i18next.t("general:Permissions")},
|
||||
{name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")},
|
||||
{name: "Properties", displayName: i18next.t("user:Properties")},
|
||||
{name: "Is online", displayName: i18next.t("user:Is online")},
|
||||
{name: "Is admin", displayName: i18next.t("user:Is admin")},
|
||||
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
|
||||
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
|
||||
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
|
||||
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
|
||||
{name: "Managed accounts", displayName: i18next.t("user:Managed accounts")},
|
||||
];
|
||||
|
||||
const getItemDisplayName = (text) => {
|
||||
const item = items.filter(item => item.name === text);
|
||||
if (item.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return item[0].displayName;
|
||||
};
|
||||
|
||||
const items = this.getItems();
|
||||
return (
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={getItemDisplayName(text)}
|
||||
options={Setting.getDeduplicatedArray(items, table, "name").map(item => Setting.getOption(item.label, item.name))}
|
||||
value={text}
|
||||
onChange={value => {
|
||||
this.updateField(table, index, "name", value);
|
||||
}} >
|
||||
{
|
||||
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>)
|
||||
}
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
|
5017
web/yarn.lock
5017
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user