mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-16 05:13:49 +08:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
50586a9716 | |||
9201992140 | |||
eb39e9e044 | |||
5b27f939b8 | |||
69ee6a6f7e | |||
bf6d5e529b | |||
55fd31f575 | |||
05c063ac24 | |||
38da63e73c | |||
cb13d693e6 | |||
d699774179 | |||
84a7fdcd07 | |||
2cd6f9df8e | |||
eea2e1d271 | |||
48c5bd942c | |||
d01d63d82a | |||
e4fd9cca92 | |||
8d531b8880 | |||
b1589e11eb | |||
b32a772a77 | |||
7e4562efe1 | |||
3a6ab4cfc6 | |||
fba4801a41 | |||
da21c92815 | |||
66c15578b1 | |||
f272be67ab |
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"}
|
||||
@ -189,7 +213,7 @@ func (c *ApiController) Finish() {
|
||||
startTime := c.Ctx.Input.GetData("startTime")
|
||||
if startTime != nil {
|
||||
latency := time.Since(startTime.(time.Time)).Milliseconds()
|
||||
object.ApiLatency.WithLabelValues(c.Ctx.Input.Method(), c.Ctx.Input.URL()).Observe(float64(latency))
|
||||
object.ApiLatency.WithLabelValues(c.Ctx.Input.URL(), c.Ctx.Input.Method()).Observe(float64(latency))
|
||||
}
|
||||
}
|
||||
c.Controller.Finish()
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
194
controllers/mfa.go
Normal file
194
controllers/mfa.go
Normal file
@ -0,0 +1,194 @@
|
||||
// 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())
|
||||
|
||||
for i, mfaProp := range mfaProps {
|
||||
mfaProps[i] = object.GetMaskedProps(mfaProp)
|
||||
}
|
||||
c.ResponseOk(mfaProps)
|
||||
}
|
@ -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
|
||||
|
@ -31,6 +31,8 @@ const (
|
||||
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,11 +179,17 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
corpEmail, err := idp.getUserCorpEmail(userId, corpAccessToken)
|
||||
if err == nil && 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"`
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
|
||||
}
|
||||
|
||||
bindPassword := string(r.AuthenticationSimple())
|
||||
bindUser, err := object.CheckUserPassword(object.CasdoorOrganization, bindUsername, bindPassword, "en")
|
||||
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
|
||||
if err != "" {
|
||||
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
|
||||
res.SetResultCode(ldap.LDAPResultInvalidCredentials)
|
||||
|
11
ldap/util.go
11
ldap/util.go
@ -56,12 +56,19 @@ func getNameAndOrgFromFilter(baseDN, filter string) (string, string, int) {
|
||||
|
||||
func getUsername(filter string) string {
|
||||
nameIndex := strings.Index(filter, "cn=")
|
||||
if nameIndex == -1 {
|
||||
nameIndex = strings.Index(filter, "uid=")
|
||||
if nameIndex == -1 {
|
||||
return "*"
|
||||
} else {
|
||||
nameIndex += 4
|
||||
}
|
||||
} else {
|
||||
nameIndex += 3
|
||||
}
|
||||
|
||||
var name string
|
||||
for i := nameIndex + 3; filter[i] != ')'; i++ {
|
||||
for i := nameIndex; filter[i] != ')'; i++ {
|
||||
name = name + string(filter[i])
|
||||
}
|
||||
return name
|
||||
@ -125,6 +132,8 @@ func getAttribute(attributeName string, user *object.User) message.AttributeValu
|
||||
return message.AttributeValue(user.Name)
|
||||
case "email":
|
||||
return message.AttributeValue(user.Email)
|
||||
case "mail":
|
||||
return message.AttributeValue(user.Email)
|
||||
case "mobile":
|
||||
return message.AttributeValue(user.Phone)
|
||||
case "userPassword":
|
||||
|
@ -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{}
|
||||
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)
|
||||
if provider.Type == "SUBMAIL" {
|
||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 !util.IsEmailValid(dest) {
|
||||
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"`
|
||||
|
@ -118,35 +118,53 @@ func getPolicies(permission *Permission) [][]string {
|
||||
return policies
|
||||
}
|
||||
|
||||
func getRolesInRole(roleId string, visited map[string]struct{}) []*Role {
|
||||
role := GetRole(roleId)
|
||||
if role == nil {
|
||||
return []*Role{}
|
||||
}
|
||||
visited[roleId] = struct{}{}
|
||||
|
||||
roles := []*Role{role}
|
||||
for _, subRole := range role.Roles {
|
||||
if _, ok := visited[subRole]; !ok {
|
||||
roles = append(roles, getRolesInRole(subRole, visited)...)
|
||||
}
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
func getGroupingPolicies(permission *Permission) [][]string {
|
||||
var groupingPolicies [][]string
|
||||
|
||||
domainExist := len(permission.Domains) > 0
|
||||
permissionId := permission.GetId()
|
||||
|
||||
for _, role := range permission.Roles {
|
||||
roleObj := GetRole(role)
|
||||
if roleObj == nil {
|
||||
continue
|
||||
}
|
||||
for _, roleId := range permission.Roles {
|
||||
visited := map[string]struct{}{}
|
||||
rolesInRole := getRolesInRole(roleId, visited)
|
||||
|
||||
for _, subUser := range roleObj.Users {
|
||||
for _, role := range rolesInRole {
|
||||
roleId := role.GetId()
|
||||
for _, subUser := range role.Users {
|
||||
if domainExist {
|
||||
for _, domain := range permission.Domains {
|
||||
groupingPolicies = append(groupingPolicies, []string{subUser, role, domain, "", "", permissionId})
|
||||
groupingPolicies = append(groupingPolicies, []string{subUser, roleId, domain, "", "", permissionId})
|
||||
}
|
||||
} else {
|
||||
groupingPolicies = append(groupingPolicies, []string{subUser, role, "", "", "", permissionId})
|
||||
groupingPolicies = append(groupingPolicies, []string{subUser, roleId, "", "", "", permissionId})
|
||||
}
|
||||
}
|
||||
|
||||
for _, subRole := range roleObj.Roles {
|
||||
for _, subRole := range role.Roles {
|
||||
if domainExist {
|
||||
for _, domain := range permission.Domains {
|
||||
groupingPolicies = append(groupingPolicies, []string{subRole, role, domain, "", "", permissionId})
|
||||
groupingPolicies = append(groupingPolicies, []string{subRole, roleId, domain, "", "", permissionId})
|
||||
}
|
||||
} else {
|
||||
groupingPolicies = append(groupingPolicies, []string{subRole, role, "", "", "", permissionId})
|
||||
groupingPolicies = append(groupingPolicies, []string{subRole, roleId, "", "", "", permissionId})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ type GaugeVecInfo struct {
|
||||
}
|
||||
|
||||
type HistogramVecInfo struct {
|
||||
Name string `json:"name"`
|
||||
Method string `json:"method"`
|
||||
Name string `json:"name"`
|
||||
Count uint64 `json:"count"`
|
||||
Latency string `json:"latency"`
|
||||
}
|
||||
|
@ -78,9 +78,12 @@ func GetMaskedProvider(provider *Provider) *Provider {
|
||||
if provider.ClientSecret != "" {
|
||||
provider.ClientSecret = "***"
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -94,10 +94,25 @@ func UpdateRole(id string, role *Role) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
visited := map[string]struct{}{}
|
||||
|
||||
permissions := GetPermissionsByRole(id)
|
||||
for _, permission := range permissions {
|
||||
removeGroupingPolicies(permission)
|
||||
removePolicies(permission)
|
||||
visited[permission.GetId()] = struct{}{}
|
||||
}
|
||||
|
||||
ancestorRoles := GetAncestorRoles(id)
|
||||
for _, r := range ancestorRoles {
|
||||
permissions := GetPermissionsByRole(r.GetId())
|
||||
for _, permission := range permissions {
|
||||
permissionId := permission.GetId()
|
||||
if _, ok := visited[permissionId]; !ok {
|
||||
removeGroupingPolicies(permission)
|
||||
visited[permissionId] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name != role.Name {
|
||||
@ -112,11 +127,25 @@ func UpdateRole(id string, role *Role) bool {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
visited = map[string]struct{}{}
|
||||
newRoleID := role.GetId()
|
||||
permissions = GetPermissionsByRole(newRoleID)
|
||||
for _, permission := range permissions {
|
||||
addGroupingPolicies(permission)
|
||||
addPolicies(permission)
|
||||
visited[permission.GetId()] = struct{}{}
|
||||
}
|
||||
|
||||
ancestorRoles = GetAncestorRoles(newRoleID)
|
||||
for _, r := range ancestorRoles {
|
||||
permissions := GetPermissionsByRole(r.GetId())
|
||||
for _, permission := range permissions {
|
||||
permissionId := permission.GetId()
|
||||
if _, ok := visited[permissionId]; !ok {
|
||||
addGroupingPolicies(permission)
|
||||
visited[permissionId] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
@ -221,3 +250,64 @@ func GetMaskedRoles(roles []*Role) []*Role {
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
func GetRolesByNamePrefix(owner string, prefix string) []*Role {
|
||||
roles := []*Role{}
|
||||
err := adapter.Engine.Where("owner=? and name like ?", owner, prefix+"%").Find(&roles)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
func GetAncestorRoles(roleId string) []*Role {
|
||||
var (
|
||||
result []*Role
|
||||
roleMap = make(map[string]*Role)
|
||||
visited = make(map[string]bool)
|
||||
)
|
||||
|
||||
owner, _ := util.GetOwnerAndNameFromIdNoCheck(roleId)
|
||||
|
||||
allRoles := GetRoles(owner)
|
||||
for _, r := range allRoles {
|
||||
roleMap[r.GetId()] = r
|
||||
}
|
||||
|
||||
// Second, find all the roles that contain father roles
|
||||
for _, r := range allRoles {
|
||||
isContain, ok := visited[r.GetId()]
|
||||
if isContain {
|
||||
result = append(result, r)
|
||||
} else if !ok {
|
||||
rId := r.GetId()
|
||||
visited[rId] = containsRole(r, roleId, roleMap, visited)
|
||||
if visited[rId] {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// containsRole is a helper function to check if a slice of roles contains a specific roleId
|
||||
func containsRole(role *Role, roleId string, roleMap map[string]*Role, visited map[string]bool) bool {
|
||||
if isContain, ok := visited[role.GetId()]; ok {
|
||||
return isContain
|
||||
}
|
||||
|
||||
for _, subRole := range role.Roles {
|
||||
if subRole == roleId {
|
||||
return true
|
||||
}
|
||||
|
||||
r, ok := roleMap[subRole]
|
||||
if ok && containsRole(r, roleId, roleMap, visited) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
if (setLoading) {
|
||||
this.setState({loading: true});
|
||||
ChatBackend.getChats("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
}
|
||||
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") {
|
||||
@ -334,8 +335,9 @@ class LoginPage extends React.Component {
|
||||
} else if (responseType === "code") {
|
||||
this.postCodeLoginAction(res);
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
const amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
|
||||
const accessToken = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
|
||||
} else if (responseType === "saml") {
|
||||
if (res.data2.method === "POST") {
|
||||
this.setState({
|
||||
@ -349,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}`);
|
||||
}
|
||||
@ -826,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,7 +67,15 @@ class ResultPage extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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)
|
||||
}
|
||||
@ -84,6 +97,8 @@ class ResultPage extends React.Component {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Alert, Button, Result} from "antd";
|
||||
import i18next from "i18next";
|
||||
import {getWechatMessageEvent} from "./AuthBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import * as Provider from "./Provider";
|
||||
@ -23,13 +24,13 @@ export function renderMessage(msg) {
|
||||
return (
|
||||
<div style={{display: "inline"}}>
|
||||
<Alert
|
||||
message="Login Error"
|
||||
message={i18next.t("application:Failed to sign in")}
|
||||
showIcon
|
||||
description={msg}
|
||||
type="error"
|
||||
action={
|
||||
<Button size="small" type="primary" danger>
|
||||
Detail
|
||||
{i18next.t("product:Detail")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@ -46,13 +47,13 @@ export function renderMessageLarge(ths, msg) {
|
||||
<Result
|
||||
style={{margin: "0px auto"}}
|
||||
status="error"
|
||||
title="There was a problem signing you in.."
|
||||
title={i18next.t("general:There was a problem signing you in..")}
|
||||
subTitle={msg}
|
||||
extra={[
|
||||
<Button type="primary" key="back" onClick={() => {
|
||||
window.history.go(-2);
|
||||
}}>
|
||||
Back
|
||||
{i18next.t("general:Back")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
@ -116,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")
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "Anwendungen, die eine Authentifizierung erfordern",
|
||||
"Avatar": "Avatar",
|
||||
"Avatar - Tooltip": "Öffentliches Avatarbild für den Benutzer",
|
||||
"Back": "Back",
|
||||
"Back Home": "Zurück nach Hause",
|
||||
"Cancel": "Abbrechen",
|
||||
"Captcha": "Captcha",
|
||||
@ -192,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",
|
||||
@ -242,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",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "Synchronisieren",
|
||||
"Syncers": "Syncers",
|
||||
"System Info": "Systeminformationen",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Dies ist eine schreibgeschützte Demo-Seite!",
|
||||
"Timestamp": "Zeitstempel",
|
||||
"Tokens": "Token",
|
||||
@ -364,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",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "Primärschlüssel in einer Tabelle, wie beispielsweise eine ID"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "API Latenz",
|
||||
"API Throughput": "API-Durchsatz",
|
||||
"About Casdoor": "Über Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Eine Identitäts- und Zugriffsverwaltung (IAM) / Single-Sign-On (SSO) Plattform mit Web-UI, die OAuth 2.0, OIDC, SAML und CAS unterstützt",
|
||||
"CPU Usage": "CPU-Auslastung",
|
||||
"Community": "Community",
|
||||
"Count": "Zählen",
|
||||
"Failed to get CPU usage": "Konnte CPU-Auslastung nicht abrufen",
|
||||
"Failed to get memory usage": "Fehler beim Abrufen der Speichernutzung",
|
||||
"Latency": "Latenz",
|
||||
"Memory Usage": "Speichernutzung",
|
||||
"Official website": "Offizielle Webseite",
|
||||
"Throughput": "Durchsatz",
|
||||
"Total Throughput": "Gesamtdurchsatz",
|
||||
"Unknown version": "Unbekannte Version",
|
||||
"Version": "Version"
|
||||
},
|
||||
@ -784,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",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "Applications that require authentication",
|
||||
"Avatar": "Avatar",
|
||||
"Avatar - Tooltip": "Public avatar image for the user",
|
||||
"Back": "Back",
|
||||
"Back Home": "Back Home",
|
||||
"Cancel": "Cancel",
|
||||
"Captcha": "Captcha",
|
||||
@ -192,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",
|
||||
@ -242,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",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "Sync",
|
||||
"Syncers": "Syncers",
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
@ -364,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",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "Table primary key, such as id"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "API Latency",
|
||||
"API Throughput": "API Throughput",
|
||||
"About Casdoor": "About Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS",
|
||||
"CPU Usage": "CPU Usage",
|
||||
"Community": "Community",
|
||||
"Count": "Count",
|
||||
"Failed to get CPU usage": "Failed to get CPU usage",
|
||||
"Failed to get memory usage": "Failed to get memory usage",
|
||||
"Latency": "Latency",
|
||||
"Memory Usage": "Memory Usage",
|
||||
"Official website": "Official website",
|
||||
"Throughput": "Throughput",
|
||||
"Total Throughput": "Total Throughput",
|
||||
"Unknown version": "Unknown version",
|
||||
"Version": "Version"
|
||||
},
|
||||
@ -784,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",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "Aplicaciones que requieren autenticación",
|
||||
"Avatar": "Avatar",
|
||||
"Avatar - Tooltip": "Imagen de avatar pública para el usuario",
|
||||
"Back": "Back",
|
||||
"Back Home": "Regreso a casa",
|
||||
"Cancel": "Cancelar",
|
||||
"Captcha": "Captcha",
|
||||
@ -192,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",
|
||||
@ -242,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",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "Sincronización",
|
||||
"Syncers": "Sincronizadores",
|
||||
"System Info": "Información del Sistema",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "¡Este es un sitio de demostración solo de lectura!",
|
||||
"Timestamp": "Marca de tiempo",
|
||||
"Tokens": "Tokens",
|
||||
@ -364,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",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "Clave primaria de la tabla, como id"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "Retraso API",
|
||||
"API Throughput": "Rendimiento API",
|
||||
"About Casdoor": "Sobre Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Una plataforma de Gestión de Identidades y Accesos (IAM) / Single-Sign-On (SSO) con una interfaz web que admite OAuth 2.0, OIDC, SAML y CAS",
|
||||
"CPU Usage": "Uso de la CPU",
|
||||
"Community": "Comunidad",
|
||||
"Count": "Contar",
|
||||
"Failed to get CPU usage": "No se pudo obtener el uso de la CPU",
|
||||
"Failed to get memory usage": "No se pudo obtener el uso de la memoria",
|
||||
"Latency": "Retraso",
|
||||
"Memory Usage": "Uso de memoria",
|
||||
"Official website": "Sitio web oficial",
|
||||
"Throughput": "Rendimiento",
|
||||
"Total Throughput": "Rendimiento total",
|
||||
"Unknown version": "Versión desconocida",
|
||||
"Version": "Versión"
|
||||
},
|
||||
@ -784,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",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "Applications qui nécessitent une authentification",
|
||||
"Avatar": "Avatar",
|
||||
"Avatar - Tooltip": "Image d'avatar public pour l'utilisateur",
|
||||
"Back": "Back",
|
||||
"Back Home": "Retour à la maison",
|
||||
"Cancel": "Annuler",
|
||||
"Captcha": "Captcha (prononcé « kæptʃə » en anglais) est un acronyme qui signifie « Completely Automated Public Turing test to tell Computers and Humans Apart ». En français, le terme est souvent traduit par « test de Turing complètement automatisé et public pour différencier les ordinateurs et les humains ». Le système est utilisé pour empêcher les spammeurs, les robots et les logiciels malveillants d'accéder aux sites Web en leur demandant de résoudre une tâche de reconnaissance de caractères ou d'images, qui peut être difficile pour les machines à comprendre mais facile pour les humains",
|
||||
@ -192,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",
|
||||
@ -242,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",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "Synchronisation",
|
||||
"Syncers": "Synchroniseurs",
|
||||
"System Info": "Information système",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Ceci est un site de démonstration en lecture seule !",
|
||||
"Timestamp": "Horodatage",
|
||||
"Tokens": "Les jetons",
|
||||
@ -364,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",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "Clé primaire de la table, telle que l'id"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "Retard API",
|
||||
"API Throughput": "Débit API",
|
||||
"About Casdoor": "À propos de Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Une plateforme de gestion d'identité et d'accès (IAM) / d'authentification unique (SSO) avec une interface utilisateur web prenant en charge OAuth 2.0, OIDC, SAML et CAS",
|
||||
"CPU Usage": "Utilisation du processeur",
|
||||
"Community": "Communauté",
|
||||
"Count": "Comptage",
|
||||
"Failed to get CPU usage": "Echec de récupération de l'utilisation du processeur (CPU)",
|
||||
"Failed to get memory usage": "Échec de l'obtention de l'utilisation de la mémoire",
|
||||
"Latency": "Retard",
|
||||
"Memory Usage": "Utilisation de la mémoire",
|
||||
"Official website": "Site web officiel",
|
||||
"Throughput": "Débit",
|
||||
"Total Throughput": "Débit total",
|
||||
"Unknown version": "Version inconnue",
|
||||
"Version": " Version"
|
||||
},
|
||||
@ -784,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",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "Aplikasi yang memerlukan autentikasi",
|
||||
"Avatar": "Avatar",
|
||||
"Avatar - Tooltip": "Gambar avatar publik untuk pengguna",
|
||||
"Back": "Back",
|
||||
"Back Home": "Kembali ke Rumah",
|
||||
"Cancel": "Membatalkan",
|
||||
"Captcha": "Captcha",
|
||||
@ -192,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",
|
||||
@ -242,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",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "Sinkronisasi",
|
||||
"Syncers": "Sinkronisasi",
|
||||
"System Info": "Informasi Sistem",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Ini adalah situs demo hanya untuk dibaca saja!",
|
||||
"Timestamp": "Waktu penanda waktu",
|
||||
"Tokens": "Token-token",
|
||||
@ -364,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",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "Kunci primer tabel, seperti id"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "API Latency",
|
||||
"API Throughput": "API Throughput",
|
||||
"About Casdoor": "Tentang Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Platform Identitas dan Akses Manajemen (IAM) / Single-Sign-On (SSO) dengan antarmuka web yang mendukung OAuth 2.0, OIDC, SAML, dan CAS",
|
||||
"CPU Usage": "Penggunaan CPU",
|
||||
"Community": "Komunitas",
|
||||
"Count": "Kiraan",
|
||||
"Failed to get CPU usage": "Gagal mendapatkan penggunaan CPU",
|
||||
"Failed to get memory usage": "Gagal mendapatkan penggunaan memori",
|
||||
"Latency": "Latency",
|
||||
"Memory Usage": "Penggunaan Memori",
|
||||
"Official website": "Situs web resmi",
|
||||
"Throughput": "Melalui",
|
||||
"Total Throughput": "Jumlah Keluaran",
|
||||
"Unknown version": "Versi tidak diketahui",
|
||||
"Version": "Versi"
|
||||
},
|
||||
@ -784,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",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "認証が必要なアプリケーション",
|
||||
"Avatar": "アバター",
|
||||
"Avatar - Tooltip": "ユーザーのパブリックアバター画像",
|
||||
"Back": "Back",
|
||||
"Back Home": "帰宅",
|
||||
"Cancel": "キャンセルします",
|
||||
"Captcha": "キャプチャ",
|
||||
@ -192,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": "名前",
|
||||
@ -242,6 +249,7 @@
|
||||
"Permissions - Tooltip": "このユーザーが所有する権限",
|
||||
"Phone": "電話",
|
||||
"Phone - Tooltip": "電話番号",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "プレビュー",
|
||||
"Preview - Tooltip": "構成されたエフェクトをプレビューする",
|
||||
"Products": "製品",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "同期",
|
||||
"Syncers": "シンカーズ",
|
||||
"System Info": "システム情報",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "これは読み取り専用のデモサイトです!",
|
||||
"Timestamp": "タイムスタンプ",
|
||||
"Tokens": "トークン",
|
||||
@ -364,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": "モデルテキスト",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "テーブルのプライマリキー、例えばid"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "API遅延",
|
||||
"API Throughput": "APIスループット",
|
||||
"About Casdoor": "Casdoorについて",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "ウェブUIを備えたアイデンティティおよびアクセス管理(IAM)/シングルサインオン(SSO)プラットフォームで、OAuth 2.0、OIDC、SAML、およびCASをサポートしています",
|
||||
"CPU Usage": "CPU使用率",
|
||||
"Community": "コミュニティ",
|
||||
"Count": "カウント",
|
||||
"Failed to get CPU usage": "CPU使用率を取得できませんでした",
|
||||
"Failed to get memory usage": "メモリ使用量を取得できませんでした",
|
||||
"Latency": "遅延",
|
||||
"Memory Usage": "メモリ使用量",
|
||||
"Official website": "公式ウェブサイト",
|
||||
"Throughput": "スループット",
|
||||
"Total Throughput": "総スループット",
|
||||
"Unknown version": "不明なバージョン",
|
||||
"Version": "バージョン"
|
||||
},
|
||||
@ -784,6 +833,7 @@
|
||||
"Location - Tooltip": "居住都市",
|
||||
"Managed accounts": "管理アカウント",
|
||||
"Modify password...": "パスワードを変更する...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "新しいメール",
|
||||
"New Password": "新しいパスワード",
|
||||
"New User": "新しいユーザー",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "인증이 필요한 애플리케이션들",
|
||||
"Avatar": "아바타",
|
||||
"Avatar - Tooltip": "사용자를 위한 공개 아바타 이미지",
|
||||
"Back": "Back",
|
||||
"Back Home": "집으로 돌아오기",
|
||||
"Cancel": "취소",
|
||||
"Captcha": "캡차",
|
||||
@ -192,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": "이름",
|
||||
@ -242,6 +249,7 @@
|
||||
"Permissions - Tooltip": "이 사용자가 소유한 권한",
|
||||
"Phone": "전화기",
|
||||
"Phone - Tooltip": "전화 번호",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "미리보기",
|
||||
"Preview - Tooltip": "구성된 효과를 미리보기합니다",
|
||||
"Products": "제품들",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "싱크",
|
||||
"Syncers": "싱크어스",
|
||||
"System Info": "시스템 정보",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "이것은 읽기 전용 데모 사이트입니다!",
|
||||
"Timestamp": "타임스탬프",
|
||||
"Tokens": "토큰",
|
||||
@ -364,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": "모델 텍스트",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "테이블의 기본 키, 예를 들어 id"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "API 지연",
|
||||
"API Throughput": "API 처리량",
|
||||
"About Casdoor": "카스도어에 대해",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "웹 UI를 지원하는 ID 및 액세스 관리 (IAM) / 단일 사인온 (SSO) 플랫폼으로 OAuth 2.0, OIDC, SAML 및 CAS를 지원합니다",
|
||||
"CPU Usage": "CPU 사용량",
|
||||
"Community": "커뮤니티",
|
||||
"Count": "카운트",
|
||||
"Failed to get CPU usage": "CPU 사용률을 얻지 못했습니다",
|
||||
"Failed to get memory usage": "메모리 사용률을 가져오는 데 실패했습니다",
|
||||
"Latency": "지연",
|
||||
"Memory Usage": "메모리 사용량",
|
||||
"Official website": "공식 웹사이트",
|
||||
"Throughput": "처리량",
|
||||
"Total Throughput": "총 처리량",
|
||||
"Unknown version": "알 수 없는 버전",
|
||||
"Version": "버전"
|
||||
},
|
||||
@ -784,6 +833,7 @@
|
||||
"Location - Tooltip": "거주 도시",
|
||||
"Managed accounts": "관리 계정",
|
||||
"Modify password...": "비밀번호 수정하기...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "새 이메일",
|
||||
"New Password": "새로운 비밀번호",
|
||||
"New User": "새로운 사용자",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "Приложения, которые требуют аутентификации",
|
||||
"Avatar": "Аватар",
|
||||
"Avatar - Tooltip": "Публичное изображение аватара пользователя",
|
||||
"Back": "Back",
|
||||
"Back Home": "Домой",
|
||||
"Cancel": "Отменить",
|
||||
"Captcha": "Капча",
|
||||
@ -192,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": "Имя",
|
||||
@ -242,6 +249,7 @@
|
||||
"Permissions - Tooltip": "Разрешения, принадлежащие этому пользователю",
|
||||
"Phone": "Телефон",
|
||||
"Phone - Tooltip": "Номер телефона",
|
||||
"Phone or email": "Phone or email",
|
||||
"Preview": "Предварительный просмотр",
|
||||
"Preview - Tooltip": "Предварительный просмотр настроенных эффектов",
|
||||
"Products": "Продукты",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "Синхронизация",
|
||||
"Syncers": "Синкеры",
|
||||
"System Info": "Системная информация",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Это демонстрационный сайт только для чтения!",
|
||||
"Timestamp": "Отметка времени",
|
||||
"Tokens": "Токены",
|
||||
@ -364,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": "Модельный текст",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "Идентификатор (id) - основной ключ таблицы"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "Задержка API",
|
||||
"API Throughput": "Пропускная способность API",
|
||||
"About Casdoor": "Об Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Платформа управления идентификацией и доступом (IAM) / единый вход в систему (SSO) с веб-интерфейсом, поддерживающая OAuth 2.0, OIDC, SAML и CAS",
|
||||
"CPU Usage": "Использование ЦПУ",
|
||||
"Community": "Сообщество",
|
||||
"Count": "Количество",
|
||||
"Failed to get CPU usage": "Не удалось получить использование процессора",
|
||||
"Failed to get memory usage": "Не удалось получить использование памяти",
|
||||
"Latency": "Задержка",
|
||||
"Memory Usage": "Использование памяти",
|
||||
"Official website": "Официальный веб-сайт",
|
||||
"Throughput": "Пропускная способность",
|
||||
"Total Throughput": "Общая пропускная способность",
|
||||
"Unknown version": "Неизвестная версия",
|
||||
"Version": "Версия"
|
||||
},
|
||||
@ -784,6 +833,7 @@
|
||||
"Location - Tooltip": "Город проживания",
|
||||
"Managed accounts": "Управляемые счета",
|
||||
"Modify password...": "Изменить пароль...",
|
||||
"Multi-factor authentication": "Multi-factor authentication",
|
||||
"New Email": "Новое электронное письмо",
|
||||
"New Password": "Новый пароль",
|
||||
"New User": "Новый пользователь",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "Các ứng dụng yêu cầu xác thực",
|
||||
"Avatar": "Ảnh đại diện",
|
||||
"Avatar - Tooltip": "Ảnh đại diện công khai cho người dùng",
|
||||
"Back": "Back",
|
||||
"Back Home": "Trở về nhà",
|
||||
"Cancel": "Hủy bỏ",
|
||||
"Captcha": "Captcha",
|
||||
@ -192,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",
|
||||
@ -242,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",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "Đồng bộ hoá",
|
||||
"Syncers": "Đồng bộ hóa",
|
||||
"System Info": "Thông tin hệ thống",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Đây là trang web giới thiệu chỉ có chức năng đọc!",
|
||||
"Timestamp": "Đánh dấu thời gian",
|
||||
"Tokens": "Mã thông báo",
|
||||
@ -364,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",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "Khóa chính của bảng, ví dụ như id"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "Độ trễ API",
|
||||
"API Throughput": "Thông lượng API",
|
||||
"About Casdoor": "Về Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Một nền tảng Quản lý Danh tính và Truy cập (IAM) / Đăng nhập Một lần (SSO) với giao diện người dùng web hỗ trợ OAuth 2.0, OIDC, SAML và CAS",
|
||||
"CPU Usage": "Sử dụng CPU",
|
||||
"Community": "Cộng đồng",
|
||||
"Count": "Đếm",
|
||||
"Failed to get CPU usage": "Không thể lấy được thông tin sử dụng CPU",
|
||||
"Failed to get memory usage": "Không thể lấy được thông tin về sử dụng bộ nhớ",
|
||||
"Latency": "Trì hoãn",
|
||||
"Memory Usage": "Sử dụng bộ nhớ",
|
||||
"Official website": "Trang web chính thức",
|
||||
"Throughput": "Thông lượng",
|
||||
"Total Throughput": "Tổng thông lượng",
|
||||
"Unknown version": "Phiên bản không rõ",
|
||||
"Version": "Phiên bản"
|
||||
},
|
||||
@ -784,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",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"Applications that require authentication": "需要认证和鉴权的应用",
|
||||
"Avatar": "头像",
|
||||
"Avatar - Tooltip": "公开展示的用户头像",
|
||||
"Back": "返回",
|
||||
"Back Home": "返回到首页",
|
||||
"Cancel": "取消",
|
||||
"Captcha": "人机验证码",
|
||||
@ -192,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": "名字",
|
||||
@ -242,6 +249,7 @@
|
||||
"Permissions - Tooltip": "该用户所拥有的权限",
|
||||
"Phone": "手机号",
|
||||
"Phone - Tooltip": "手机号",
|
||||
"Phone or email": "手机或邮箱",
|
||||
"Preview": "预览",
|
||||
"Preview - Tooltip": "可预览所配置的效果",
|
||||
"Products": "商品",
|
||||
@ -280,6 +288,7 @@
|
||||
"Sync": "同步",
|
||||
"Syncers": "同步器",
|
||||
"System Info": "系统信息",
|
||||
"There was a problem signing you in..": "登录时遇到问题..",
|
||||
"This is a read-only demo site!": "这是一个只读演示站点!",
|
||||
"Timestamp": "时间戳",
|
||||
"Tokens": "令牌",
|
||||
@ -364,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": "模型文本",
|
||||
@ -542,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",
|
||||
@ -701,14 +744,20 @@
|
||||
"Table primary key - Tooltip": "表主键,如id等"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "API 延迟",
|
||||
"API Throughput": "API 吞吐量",
|
||||
"About Casdoor": "关于Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "一个支持OAuth 2.0、OIDC、SAML和CAS的Web UI优先的身份和访问管理(IAM)/单点登录(SSO)平台",
|
||||
"CPU Usage": "CPU使用率",
|
||||
"Community": "社区",
|
||||
"Count": "次数",
|
||||
"Failed to get CPU usage": "获取CPU使用率失败",
|
||||
"Failed to get memory usage": "获取内存使用率失败",
|
||||
"Latency": "延迟",
|
||||
"Memory Usage": "内存使用率",
|
||||
"Official website": "官方网站",
|
||||
"Throughput": "吞吐量",
|
||||
"Total Throughput": "总吞吐量",
|
||||
"Unknown version": "未知版本",
|
||||
"Version": "版本"
|
||||
},
|
||||
@ -784,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>
|
||||
);
|
||||
},
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Table} from "antd";
|
||||
import i18next from "i18next";
|
||||
|
||||
class PrometheusInfoTable extends React.Component {
|
||||
constructor(props) {
|
||||
@ -25,39 +26,39 @@ class PrometheusInfoTable extends React.Component {
|
||||
render() {
|
||||
const latencyColumns = [
|
||||
{
|
||||
title: "Name",
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Method",
|
||||
title: i18next.t("general:Method"),
|
||||
dataIndex: "method",
|
||||
key: "method",
|
||||
},
|
||||
{
|
||||
title: "Count",
|
||||
title: i18next.t("system:Count"),
|
||||
dataIndex: "count",
|
||||
key: "count",
|
||||
},
|
||||
{
|
||||
title: "Latency(ms)",
|
||||
title: i18next.t("system:Latency") + "(ms)",
|
||||
dataIndex: "latency",
|
||||
key: "latency",
|
||||
},
|
||||
];
|
||||
const throughputColumns = [
|
||||
{
|
||||
title: "Name",
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Method",
|
||||
title: i18next.t("general:Method"),
|
||||
dataIndex: "method",
|
||||
key: "method",
|
||||
},
|
||||
{
|
||||
title: "Throughput",
|
||||
title: i18next.t("system:Throughput"),
|
||||
dataIndex: "throughput",
|
||||
key: "throughput",
|
||||
},
|
||||
@ -71,7 +72,7 @@ class PrometheusInfoTable extends React.Component {
|
||||
} else if (this.state.table === "throughput") {
|
||||
return (
|
||||
<div style={{height: "300px", overflow: "auto"}}>
|
||||
Total Throughput: {this.props.prometheusInfo.totalThroughput}
|
||||
{i18next.t("system:Total Throughput")}: {this.props.prometheusInfo.totalThroughput}
|
||||
<Table columns={throughputColumns} dataSource={this.props.prometheusInfo.apiThroughput} pagination={false} />
|
||||
</div>
|
||||
);
|
||||
|
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