Compare commits

...

16 Commits

Author SHA1 Message Date
Dzung Do
984a69cb4b feat: fix wrong Vietnamese flag (#1724)
* fix wrong Vietnam country code

* fix wrong Vietnam country code

* fix wrong Vietnam country code

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

* feat: add wechatpay support.

* Update wechatv3pay.go

* fix: update format.

* Update wechatv3pay.go

* Update wechatv3pay.go

* Update wechatv3pay.go

* fix: update file format.

* fix: improve the front of wechat payment.

* fix: change clientId2 to clientId.

* fix: fix the code format.

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

* fix: delete blank line
2023-04-09 10:35:30 +08:00
jivfly
3d5a645a3b feat: fix field name error of termsOfUse (#1715) 2023-04-09 01:01:04 +08:00
Gucheng Wang
4ad21e7781 fix: fix WeCom provider method 2023-04-07 01:10:46 +08:00
Yaodong Yu
b99a0c3ca2 feat: optimize the "forget password" page (#1709) 2023-04-06 23:06:18 +08:00
Yaodong Yu
e1842f6b80 feat: fix LDAP server handle filter without CN field as * (#1705)
* fix: set ldap server default filter name as *

* fix: default use built-in organization to bind

* chore: use cache reduce the ci test time
2023-04-04 20:51:28 +08:00
Max Baier
0781a3835d feat: improve i18n to have proper German translation in web/ (#1702) 2023-04-02 10:52:30 +08:00
Yang Luo
98a99f0215 Fix bug in getMemoryUsage() 2023-04-02 10:50:41 +08:00
Yang Luo
681b086de0 Fix session page highlight 2023-04-01 17:36:50 +08:00
Yaodong Yu
cdcc0b39e2 feat: filter not selected provider item (#1701) 2023-04-01 10:22:18 +08:00
Gucheng Wang
8eb68ba817 fix: fix AAD single-tenant mode bug 2023-03-31 19:24:03 +08:00
Yang Luo
8d1ae4ea08 Fix organization page bug 2023-03-31 18:35:57 +08:00
57 changed files with 2423 additions and 498 deletions

View File

@@ -9,18 +9,19 @@ jobs:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_DATABASE: casdoor
MYSQL_ROOT_PASSWORD: 123456
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
image: mysql:5.7
env:
MYSQL_DATABASE: casdoor
MYSQL_ROOT_PASSWORD: 123456
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '^1.16.5'
cache-dependency-path: ./go.mod
- name: Tests
run: |
go test -v $(go list ./...) -tags skipCi
@@ -31,14 +32,12 @@ jobs:
runs-on: ubuntu-latest
needs: [ go-tests ]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
# cache
- uses: c-hive/gha-yarn-cache@v2
with:
directory: ./web
cache: 'yarn'
cache-dependency-path: ./web/yarn.lock
- run: yarn install && CI=false yarn run build
working-directory: ./web
@@ -47,10 +46,11 @@ jobs:
runs-on: ubuntu-latest
needs: [ go-tests ]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '^1.16.5'
cache-dependency-path: ./go.mod
- run: go version
- name: Build
run: |
@@ -63,13 +63,14 @@ jobs:
needs: [ go-tests ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: '^1.16.5'
cache-dependency-path: ./go.mod
# gen a dummy config file
- run: touch dummy.yml
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
@@ -82,35 +83,35 @@ jobs:
needs: [ go-tests ]
services:
mysql:
image: mysql:5.7
env:
MYSQL_DATABASE: casdoor
MYSQL_ROOT_PASSWORD: 123456
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
image: mysql:5.7
env:
MYSQL_DATABASE: casdoor
MYSQL_ROOT_PASSWORD: 123456
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '^1.16.5'
- uses: actions/setup-node@v2
with:
node-version: 16
- name: back start
cache-dependency-path: ./go.mod
- name: start backend
run: nohup go run ./main.go &
working-directory: ./
- name: front install
run: yarn install
working-directory: ./web
- name: front start
run: nohup yarn start &
working-directory: ./web
- uses: cypress-io/github-action@v4
- uses: actions/setup-node@v3
with:
working-directory: ./web
node-version: 16
cache: 'yarn'
cache-dependency-path: ./web/yarn.lock
- run: yarn install
working-directory: ./web
- uses: cypress-io/github-action@v5
with:
start: yarn start
wait-on: 'http://localhost:7001'
wait-on-timeout: 180
working-directory: ./web
- uses: actions/upload-artifact@v3
if: failure()
@@ -121,7 +122,7 @@ jobs:
if: always()
with:
name: cypress-videos
path: ./web/cypress/videos
path: ./web/cypress/videos
release-and-push:
name: Release And Push
@@ -130,11 +131,11 @@ jobs:
needs: [ frontend, backend, linter, e2e ]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: -1
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 16
@@ -166,10 +167,10 @@ jobs:
elif [ ${old_array[1]} != ${new_array[1]} ]
then
echo ::set-output name=push::'true'
else
echo ::set-output name=push::'false'
fi
- name: Set up QEMU

View File

@@ -108,6 +108,7 @@ p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, *
p, *, *, GET, /api/get-captcha, *, *
p, *, *, POST, /api/verify-captcha, *, *
p, *, *, POST, /api/verify-code, *, *
p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, *

View File

@@ -20,5 +20,5 @@ staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
ldapServerPort = 389
languages = en,zh,es,fr,de,id,ja,ko,ru,vi
languages = en,zh,es,fr,de,id,ja,ko,ru,vn
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}

View File

@@ -246,33 +246,14 @@ func (c *ApiController) Login() {
var msg string
if form.Password == "" {
var verificationCodeType string
var checkResult string
if form.Name != "" {
user = object.GetUserByFields(form.Organization, form.Name)
}
// check result through Email or Phone
var checkDest string
if strings.Contains(form.Username, "@") {
verificationCodeType = "email"
if user != nil && util.GetMaskedEmail(user.Email) == form.Username {
form.Username = user.Email
}
checkDest = form.Username
} else {
verificationCodeType = "phone"
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.Username = user.Phone
}
}
if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
return
}
if verificationCodeType == "phone" {
verificationCodeType := object.GetVerifyType(form.Username)
var checkDest string
if verificationCodeType == object.VerifyTypePhone {
form.CountryCode = user.GetCountryCode(form.CountryCode)
var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
@@ -281,7 +262,8 @@ func (c *ApiController) Login() {
}
}
checkResult = object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage())
// check result through Email or Phone
checkResult := object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
return

123
controllers/chat.go Normal file
View File

@@ -0,0 +1,123 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetChats
// @Title GetChats
// @Tag Chat API
// @Description get chats
// @Param owner query string true "The owner of chats"
// @Success 200 {array} object.Chat The Response object
// @router /get-chats [get]
func (c *ApiController) GetChats() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedChats(object.GetChats(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetChatCount(owner, field, value)))
chats := object.GetMaskedChats(object.GetPaginationChats(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(chats, paginator.Nums())
}
}
// GetChat
// @Title GetChat
// @Tag Chat API
// @Description get chat
// @Param id query string true "The id ( owner/name ) of the chat"
// @Success 200 {object} object.Chat The Response object
// @router /get-chat [get]
func (c *ApiController) GetChat() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedChat(object.GetChat(id))
c.ServeJSON()
}
// UpdateChat
// @Title UpdateChat
// @Tag Chat API
// @Description update chat
// @Param id query string true "The id ( owner/name ) of the chat"
// @Param body body object.Chat true "The details of the chat"
// @Success 200 {object} controllers.Response The Response object
// @router /update-chat [post]
func (c *ApiController) UpdateChat() {
id := c.Input().Get("id")
var chat object.Chat
err := json.Unmarshal(c.Ctx.Input.RequestBody, &chat)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateChat(id, &chat))
c.ServeJSON()
}
// AddChat
// @Title AddChat
// @Tag Chat API
// @Description add chat
// @Param body body object.Chat true "The details of the chat"
// @Success 200 {object} controllers.Response The Response object
// @router /add-chat [post]
func (c *ApiController) AddChat() {
var chat object.Chat
err := json.Unmarshal(c.Ctx.Input.RequestBody, &chat)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddChat(&chat))
c.ServeJSON()
}
// DeleteChat
// @Title DeleteChat
// @Tag Chat API
// @Description delete chat
// @Param body body object.Chat true "The details of the chat"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-chat [post]
func (c *ApiController) DeleteChat() {
var chat object.Chat
err := json.Unmarshal(c.Ctx.Input.RequestBody, &chat)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteChat(&chat))
c.ServeJSON()
}

123
controllers/message.go Normal file
View File

@@ -0,0 +1,123 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetMessages
// @Title GetMessages
// @Tag Message API
// @Description get messages
// @Param owner query string true "The owner of messages"
// @Success 200 {array} object.Message The Response object
// @router /get-messages [get]
func (c *ApiController) GetMessages() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedMessages(object.GetMessages(owner))
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetMessageCount(owner, field, value)))
messages := object.GetMaskedMessages(object.GetPaginationMessages(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(messages, paginator.Nums())
}
}
// GetMessage
// @Title GetMessage
// @Tag Message API
// @Description get message
// @Param id query string true "The id ( owner/name ) of the message"
// @Success 200 {object} object.Message The Response object
// @router /get-message [get]
func (c *ApiController) GetMessage() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedMessage(object.GetMessage(id))
c.ServeJSON()
}
// UpdateMessage
// @Title UpdateMessage
// @Tag Message API
// @Description update message
// @Param id query string true "The id ( owner/name ) of the message"
// @Param body body object.Message true "The details of the message"
// @Success 200 {object} controllers.Response The Response object
// @router /update-message [post]
func (c *ApiController) UpdateMessage() {
id := c.Input().Get("id")
var message object.Message
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateMessage(id, &message))
c.ServeJSON()
}
// AddMessage
// @Title AddMessage
// @Tag Message API
// @Description add message
// @Param body body object.Message true "The details of the message"
// @Success 200 {object} controllers.Response The Response object
// @router /add-message [post]
func (c *ApiController) AddMessage() {
var message object.Message
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddMessage(&message))
c.ServeJSON()
}
// DeleteMessage
// @Title DeleteMessage
// @Tag Message API
// @Description delete message
// @Param body body object.Message true "The details of the message"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-message [post]
func (c *ApiController) DeleteMessage() {
var message object.Message
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteMessage(&message))
c.ServeJSON()
}

View File

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

View File

@@ -20,8 +20,7 @@ import (
"github.com/casdoor/casdoor/object"
)
func checkPermissionForUpdateUser(userId string, newUser object.User, c *ApiController) (bool, string) {
oldUser := object.GetUser(userId)
func checkPermissionForUpdateUser(oldUser, newUser *object.User, c *ApiController) (bool, string) {
organization := object.GetOrganizationByUser(oldUser)
var itemsChanged []*object.AccountItem

View File

@@ -15,6 +15,7 @@
package controllers
import (
"encoding/json"
"errors"
"fmt"
"strings"
@@ -110,7 +111,7 @@ func (c *ApiController) SendVerificationCode() {
sendResp := errors.New("invalid dest type")
switch destType {
case "email":
case object.VerifyTypeEmail:
if !util.IsEmailValid(dest) {
c.ResponseError(c.T("check:Email is invalid"))
return
@@ -132,7 +133,7 @@ func (c *ApiController) SendVerificationCode() {
provider := application.GetEmailProvider()
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
case "phone":
case object.VerifyTypePhone:
if method == LoginVerification || method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == dest {
dest = user.Phone
@@ -187,7 +188,7 @@ func (c *ApiController) ResetEmailOrPhone() {
checkDest := dest
organization := object.GetOrganizationByUser(user)
if destType == "phone" {
if destType == object.VerifyTypePhone {
if object.HasUserByField(user.Owner, "phone", dest) {
c.ResponseError(c.T("check:Phone already exists"))
return
@@ -207,7 +208,7 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), user.CountryCode))
return
}
} else if destType == "email" {
} else if destType == object.VerifyTypeEmail {
if object.HasUserByField(user.Owner, "email", dest) {
c.ResponseError(c.T("check:Email already exists"))
return
@@ -230,10 +231,10 @@ func (c *ApiController) ResetEmailOrPhone() {
}
switch destType {
case "email":
case object.VerifyTypeEmail:
user.Email = dest
object.SetUserField(user, "email", user.Email)
case "phone":
case object.VerifyTypePhone:
user.Phone = dest
object.SetUserField(user, "phone", user.Phone)
default:
@@ -245,6 +246,60 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseOk()
}
// VerifyCode
// @Tag Account API
// @Title VerifyCode
// @router /api/verify-code [post]
func (c *ApiController) VerifyCode() {
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
c.ResponseError(err.Error())
return
}
var user *object.User
if form.Name != "" {
user = object.GetUserByFields(form.Organization, form.Name)
}
var checkDest string
if strings.Contains(form.Username, "@") {
if user != nil && util.GetMaskedEmail(user.Email) == form.Username {
form.Username = user.Email
}
checkDest = form.Username
} else {
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.Username = user.Phone
}
}
if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
return
}
verificationCodeType := object.GetVerifyType(form.Username)
if verificationCodeType == object.VerifyTypePhone {
form.CountryCode = user.GetCountryCode(form.CountryCode)
var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), form.CountryCode))
return
}
}
if result := object.CheckVerificationCode(checkDest, form.Code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
}
object.DisableVerificationCode(checkDest)
c.SetSession("verifiedCode", form.Code)
c.ResponseOk()
}
// VerifyCaptcha ...
// @Title VerifyCaptcha
// @Tag Verification API

2
go.mod
View File

@@ -23,7 +23,7 @@ require (
github.com/go-mysql-org/go-mysql v1.7.0
github.com/go-pay/gopay v1.5.72
github.com/go-sql-driver/mysql v1.6.0
github.com/go-webauthn/webauthn v0.8.2
github.com/go-webauthn/webauthn v0.5.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0

10
go.sum
View File

@@ -222,10 +222,10 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-webauthn/revoke v0.1.9 h1:gSJ1ckA9VaKA2GN4Ukp+kiGTk1/EXtaDb1YE8RknbS0=
github.com/go-webauthn/revoke v0.1.9/go.mod h1:j6WKPnv0HovtEs++paan9g3ar46gm1NarktkXBaPR+w=
github.com/go-webauthn/webauthn v0.8.2 h1:8KLIbpldjz9KVGHfqEgJNbkhd7bbRXhNw4QWFJE15oA=
github.com/go-webauthn/webauthn v0.8.2/go.mod h1:d+ezx/jMCNDiqSMzOchuynKb9CVU1NM9BumOnokfcVQ=
github.com/go-webauthn/revoke v0.1.6 h1:3tv+itza9WpX5tryRQx4GwxCCBrCIiJ8GIkOhxiAmmU=
github.com/go-webauthn/revoke v0.1.6/go.mod h1:TB4wuW4tPlwgF3znujA96F70/YSQXHPPWl7vgY09Iy8=
github.com/go-webauthn/webauthn v0.5.0 h1:Tbmp37AGIhYbQmcy2hEffo3U3cgPClqvxJ7cLUnF7Rc=
github.com/go-webauthn/webauthn v0.5.0/go.mod h1:0CBq/jNfPS9l033j4AxMk8K8MluiMsde9uGNSPFLEVE=
github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -234,6 +234,7 @@ github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptG
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
@@ -658,6 +659,7 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=

View File

@@ -31,7 +31,7 @@ func TestGenerateI18nFrontend(t *testing.T) {
applyToOtherLanguage("frontend", "ja", data)
applyToOtherLanguage("frontend", "ko", data)
applyToOtherLanguage("frontend", "ru", data)
applyToOtherLanguage("frontend", "vi", data)
applyToOtherLanguage("frontend", "vn", data)
}
func TestGenerateI18nBackend(t *testing.T) {
@@ -46,5 +46,5 @@ func TestGenerateI18nBackend(t *testing.T) {
applyToOtherLanguage("backend", "ja", data)
applyToOtherLanguage("backend", "ko", data)
applyToOtherLanguage("backend", "ru", data)
applyToOtherLanguage("backend", "vi", data)
applyToOtherLanguage("backend", "vn", data)
}

View File

@@ -88,7 +88,7 @@ type GothIdProvider struct {
Session goth.Session
}
func NewGothIdProvider(providerType string, clientId string, clientSecret string, redirectUrl string) *GothIdProvider {
func NewGothIdProvider(providerType string, clientId string, clientSecret string, redirectUrl string, hostUrl string) *GothIdProvider {
var idp GothIdProvider
switch providerType {
case "Amazon":
@@ -102,8 +102,13 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
Session: &apple.Session{},
}
case "AzureAD":
domain := "common"
if hostUrl != "" {
domain = hostUrl
}
idp = GothIdProvider{
Provider: azureadv2.New(clientId, clientSecret, redirectUrl, azureadv2.ProviderOptions{Tenant: "common"}),
Provider: azureadv2.New(clientId, clientSecret, redirectUrl, azureadv2.ProviderOptions{Tenant: azureadv2.TenantType(domain)}),
Session: &azureadv2.Session{},
}
case "Auth0":

View File

@@ -90,7 +90,7 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
} else if typ == "Douyin" {
return NewDouyinIdProvider(clientId, clientSecret, redirectUrl)
} else if isGothSupport(typ) {
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Bilibili" {
return NewBilibiliIdProvider(clientId, clientSecret, redirectUrl)
}

View File

@@ -12,7 +12,7 @@
"defaultAvatar": "",
"defaultApplication": "",
"tags": [],
"languages": ["en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi"],
"languages": ["en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vn"],
"masterPassword": "",
"initScore": 2000,
"enableSoftDeletion": false,

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
package ldap
import (
"fmt"
@@ -20,76 +20,78 @@ import (
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
"github.com/forestmgy/ldapserver"
ldap "github.com/forestmgy/ldapserver"
"github.com/lor00x/goldap/message"
)
func StartLdapServer() {
server := ldapserver.NewServer()
routes := ldapserver.NewRouteMux()
server := ldap.NewServer()
routes := ldap.NewRouteMux()
routes.Bind(handleBind)
routes.Search(handleSearch).Label(" SEARCH****")
server.Handle(routes)
server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
err := server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
if err != nil {
return
}
}
func handleBind(w ldapserver.ResponseWriter, m *ldapserver.Message) {
func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
r := m.GetBindRequest()
res := ldapserver.NewBindResponse(ldapserver.LDAPResultSuccess)
res := ldap.NewBindResponse(ldap.LDAPResultSuccess)
if r.AuthenticationChoice() == "simple" {
bindusername, bindorg, err := object.GetNameAndOrgFromDN(string(r.Name()))
bindUsername, bindOrg, err := getNameAndOrgFromDN(string(r.Name()))
if err != "" {
log.Printf("Bind failed ,ErrMsg=%s", err)
res.SetResultCode(ldapserver.LDAPResultInvalidDNSyntax)
res.SetResultCode(ldap.LDAPResultInvalidDNSyntax)
res.SetDiagnosticMessage("bind failed ErrMsg: " + err)
w.Write(res)
return
}
bindpassword := string(r.AuthenticationSimple())
binduser, err := object.CheckUserPassword(bindorg, bindusername, bindpassword, "en")
bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(object.CasdoorOrganization, bindUsername, bindPassword, "en")
if err != "" {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
res.SetResultCode(ldapserver.LDAPResultInvalidCredentials)
res.SetResultCode(ldap.LDAPResultInvalidCredentials)
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err)
w.Write(res)
return
}
if bindorg == "built-in" {
if bindOrg == "built-in" || bindUser.IsGlobalAdmin {
m.Client.IsGlobalAdmin, m.Client.IsOrgAdmin = true, true
} else if binduser.IsAdmin {
} else if bindUser.IsAdmin {
m.Client.IsOrgAdmin = true
}
m.Client.IsAuthenticated = true
m.Client.UserName = bindusername
m.Client.OrgName = bindorg
m.Client.UserName = bindUsername
m.Client.OrgName = bindOrg
} else {
res.SetResultCode(ldapserver.LDAPResultAuthMethodNotSupported)
res.SetResultCode(ldap.LDAPResultAuthMethodNotSupported)
res.SetDiagnosticMessage("Authentication method not supported,Please use Simple Authentication")
}
w.Write(res)
}
func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) {
res := ldapserver.NewSearchResultDoneResponse(ldapserver.LDAPResultSuccess)
func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess)
if !m.Client.IsAuthenticated {
res.SetResultCode(ldapserver.LDAPResultUnwillingToPerform)
res.SetResultCode(ldap.LDAPResultUnwillingToPerform)
w.Write(res)
return
}
r := m.GetSearchRequest()
if r.FilterString() == "(objectClass=*)" {
w.Write(res)
return
}
name, org, errCode := object.GetUserNameAndOrgFromBaseDnAndFilter(string(r.BaseObject()), r.FilterString())
if errCode != ldapserver.LDAPResultSuccess {
res.SetResultCode(errCode)
w.Write(res)
return
}
// Handle Stop Signal (server stop / client disconnected / Abandoned request....)
select {
case <-m.Done:
@@ -97,16 +99,17 @@ func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) {
return
default:
}
users, errCode := object.GetFilteredUsers(m, name, org)
if errCode != ldapserver.LDAPResultSuccess {
res.SetResultCode(errCode)
users, code := GetFilteredUsers(m)
if code != ldap.LDAPResultSuccess {
res.SetResultCode(code)
w.Write(res)
return
}
for i := 0; i < len(users); i++ {
user := users[i]
for _, user := range users {
dn := fmt.Sprintf("cn=%s,%s", user.Name, string(r.BaseObject()))
e := ldapserver.NewSearchResultEntry(dn)
e := ldap.NewSearchResultEntry(dn)
e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Name))
e.AddAttribute("email", message.AttributeValue(user.Email))
@@ -117,22 +120,3 @@ func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) {
}
w.Write(res)
}
// get user password with hash type prefix
// TODO not handle salt yet
// @return {md5}5f4dcc3b5aa765d61d8327deb882cf99
func getUserPasswordWithType(user *object.User) string {
org := object.GetOrganizationByUser(user)
if org.PasswordType == "" || org.PasswordType == "plain" {
return user.Password
}
prefix := org.PasswordType
if prefix == "salt" {
prefix = "sha256"
} else if prefix == "md5-salt" {
prefix = "md5"
} else if prefix == "pbkdf2-salt" {
prefix = "pbkdf2"
}
return fmt.Sprintf("{%s}%s", prefix, user.Password)
}

116
ldap/util.go Normal file
View File

@@ -0,0 +1,116 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ldap
import (
"fmt"
"log"
"strings"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
ldap "github.com/forestmgy/ldapserver"
)
func getNameAndOrgFromDN(DN string) (string, string, string) {
DNFields := strings.Split(DN, ",")
params := make(map[string]string, len(DNFields))
for _, field := range DNFields {
if strings.Contains(field, "=") {
k := strings.Split(field, "=")
params[k[0]] = k[1]
}
}
if params["cn"] == "" {
return "", "", "please use Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com"
}
if params["ou"] == "" {
return params["cn"], object.CasdoorOrganization, ""
}
return params["cn"], params["ou"], ""
}
func getNameAndOrgFromFilter(baseDN, filter string) (string, string, int) {
if !strings.Contains(baseDN, "ou=") {
return "", "", ldap.LDAPResultInvalidDNSyntax
}
name, org, _ := getNameAndOrgFromDN(fmt.Sprintf("cn=%s,", getUsername(filter)) + baseDN)
return name, org, ldap.LDAPResultSuccess
}
func getUsername(filter string) string {
nameIndex := strings.Index(filter, "cn=")
if nameIndex == -1 {
return "*"
}
var name string
for i := nameIndex + 3; filter[i] != ')'; i++ {
name = name + string(filter[i])
}
return name
}
func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int) {
r := m.GetSearchRequest()
name, org, code := getNameAndOrgFromFilter(string(r.BaseObject()), r.FilterString())
if code != ldap.LDAPResultSuccess {
return nil, code
}
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org'
if m.Client.IsGlobalAdmin && org == "*" {
filteredUsers = object.GetGlobalUsers()
return filteredUsers, ldap.LDAPResultSuccess
}
if m.Client.IsGlobalAdmin || org == m.Client.OrgName {
filteredUsers = object.GetUsers(org)
return filteredUsers, ldap.LDAPResultSuccess
} else {
return nil, ldap.LDAPResultInsufficientAccessRights
}
} else {
hasPermission, err := object.CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), true, "en")
if !hasPermission {
log.Printf("ErrMsg = %v", err.Error())
return nil, ldap.LDAPResultInsufficientAccessRights
}
user := object.GetUser(util.GetId(org, name))
filteredUsers = append(filteredUsers, user)
return filteredUsers, ldap.LDAPResultSuccess
}
}
// get user password with hash type prefix
// TODO not handle salt yet
// @return {md5}5f4dcc3b5aa765d61d8327deb882cf99
func getUserPasswordWithType(user *object.User) string {
org := object.GetOrganizationByUser(user)
if org.PasswordType == "" || org.PasswordType == "plain" {
return user.Password
}
prefix := org.PasswordType
if prefix == "salt" {
prefix = "sha256"
} else if prefix == "md5-salt" {
prefix = "md5"
} else if prefix == "pbkdf2-salt" {
prefix = "pbkdf2"
}
return fmt.Sprintf("{%s}%s", prefix, user.Password)
}

View File

@@ -23,7 +23,7 @@ import (
_ "github.com/beego/beego/session/redis"
"github.com/casdoor/casdoor/authz"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/controllers"
"github.com/casdoor/casdoor/ldap"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/routers"
@@ -81,7 +81,7 @@ func main() {
// logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false)
go controllers.StartLdapServer()
go ldap.StartLdapServer()
beego.Run(fmt.Sprintf(":%v", port))
}

View File

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

141
object/chat.go Normal file
View File

@@ -0,0 +1,141 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type Chat struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Organization string `xorm:"varchar(100)" json:"organization"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
User1 string `xorm:"varchar(100)" json:"user1"`
User2 string `xorm:"varchar(100)" json:"user2"`
Users []string `xorm:"varchar(100)" json:"users"`
MessageCount int `json:"messageCount"`
}
func GetMaskedChat(chat *Chat) *Chat {
if chat == nil {
return nil
}
return chat
}
func GetMaskedChats(chats []*Chat) []*Chat {
for _, chat := range chats {
chat = GetMaskedChat(chat)
}
return chats
}
func GetChatCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Chat{})
if err != nil {
panic(err)
}
return int(count)
}
func GetChats(owner string) []*Chat {
chats := []*Chat{}
err := adapter.Engine.Desc("created_time").Find(&chats, &Chat{Owner: owner})
if err != nil {
panic(err)
}
return chats
}
func GetPaginationChats(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Chat {
chats := []*Chat{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&chats)
if err != nil {
panic(err)
}
return chats
}
func getChat(owner string, name string) *Chat {
if owner == "" || name == "" {
return nil
}
chat := Chat{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&chat)
if err != nil {
panic(err)
}
if existed {
return &chat
} else {
return nil
}
}
func GetChat(id string) *Chat {
owner, name := util.GetOwnerAndNameFromId(id)
return getChat(owner, name)
}
func UpdateChat(id string, chat *Chat) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getChat(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(chat)
if err != nil {
panic(err)
}
return affected != 0
}
func AddChat(chat *Chat) bool {
affected, err := adapter.Engine.Insert(chat)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteChat(chat *Chat) bool {
affected, err := adapter.Engine.ID(core.PK{chat.Owner, chat.Name}).Delete(&Chat{})
if err != nil {
panic(err)
}
return affected != 0
}
func (p *Chat) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

View File

@@ -250,11 +250,13 @@ func filterField(field string) bool {
return reFieldWhiteList.MatchString(field)
}
func CheckUserPermission(requestUserId, userId, userOwner string, strict bool, lang string) (bool, error) {
func CheckUserPermission(requestUserId, userId string, strict bool, lang string) (bool, error) {
if requestUserId == "" {
return false, fmt.Errorf(i18n.Translate(lang, "general:Please login first"))
}
userOwner := util.GetOwnerFromId(userId)
if userId != "" {
targetUser := GetUser(userId)
if targetUser == nil {
@@ -340,7 +342,7 @@ func CheckUsername(username string, lang string) string {
return ""
}
func CheckUpdateUser(oldUser *User, user *User, lang string) string {
func CheckUpdateUser(oldUser, user *User, lang string) string {
if user.DisplayName == "" {
return i18n.Translate(lang, "user:Display name cannot be empty")
}

View File

@@ -89,7 +89,7 @@ func initBuiltInOrganization() bool {
CountryCodes: []string{"US", "ES", "CN", "FR", "DE", "GB", "JP", "KR", "VN", "ID", "SG", "IN"},
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Tags: []string{},
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi"},
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vn"},
InitScore: 2000,
AccountItems: getBuiltInAccountItems(),
EnableSoftDeletion: false,

View File

@@ -115,7 +115,7 @@ func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
}
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
SearchFilter := "(objectclass=*)"
SearchFilter := "(objectClass=*)"
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
searchReq := goldap.NewSearchRequest("",
@@ -126,7 +126,7 @@ func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
return false, err
}
if len(searchResult.Entries) == 0 {
return false, errors.New("no result")
return false, nil
}
isMicrosoft := false
var ldapServerType ldapServerType

View File

@@ -1,74 +0,0 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"log"
"strings"
"github.com/forestmgy/ldapserver"
)
func GetNameAndOrgFromDN(DN string) (string, string, string) {
DNValue := strings.Split(DN, ",")
if len(DNValue) == 1 || strings.ToLower(DNValue[0])[0] != 'c' || strings.ToLower(DNValue[1])[0] != 'o' {
return "", "", "please use correct Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com"
}
return DNValue[0][3:], DNValue[1][3:], ""
}
func GetUserNameAndOrgFromBaseDnAndFilter(baseDN, filter string) (string, string, int) {
if !strings.Contains(baseDN, "ou=") || !strings.Contains(filter, "cn=") {
return "", "", ldapserver.LDAPResultInvalidDNSyntax
}
name := getUserNameFromFilter(filter)
_, org, _ := GetNameAndOrgFromDN(fmt.Sprintf("cn=%s,", name) + baseDN)
errCode := ldapserver.LDAPResultSuccess
return name, org, errCode
}
func getUserNameFromFilter(filter string) string {
nameIndex := strings.Index(filter, "cn=")
var name string
for i := nameIndex + 3; filter[i] != ')'; i++ {
name = name + string(filter[i])
}
return name
}
func GetFilteredUsers(m *ldapserver.Message, name, org string) ([]*User, int) {
var filteredUsers []*User
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org'
if m.Client.OrgName == "built-in" && org == "*" {
filteredUsers = GetGlobalUsers()
return filteredUsers, ldapserver.LDAPResultSuccess
} else if m.Client.OrgName == "built-in" || org == m.Client.OrgName {
filteredUsers = GetUsers(org)
return filteredUsers, ldapserver.LDAPResultSuccess
} else {
return nil, ldapserver.LDAPResultInsufficientAccessRights
}
} else {
hasPermission, err := CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), org, true, "en")
if !hasPermission {
log.Printf("ErrMsg = %v", err.Error())
return nil, ldapserver.LDAPResultInsufficientAccessRights
}
user := getUser(org, name)
filteredUsers = append(filteredUsers, user)
return filteredUsers, ldapserver.LDAPResultSuccess
}
}

138
object/message.go Normal file
View File

@@ -0,0 +1,138 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type Message struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Organization string `xorm:"varchar(100)" json:"organization"`
Chat string `xorm:"varchar(100)" json:"chat"`
Author string `xorm:"varchar(100)" json:"author"`
Text string `xorm:"mediumtext" json:"text"`
}
func GetMaskedMessage(message *Message) *Message {
if message == nil {
return nil
}
return message
}
func GetMaskedMessages(messages []*Message) []*Message {
for _, message := range messages {
message = GetMaskedMessage(message)
}
return messages
}
func GetMessageCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Message{})
if err != nil {
panic(err)
}
return int(count)
}
func GetMessages(owner string) []*Message {
messages := []*Message{}
err := adapter.Engine.Desc("created_time").Find(&messages, &Message{Owner: owner})
if err != nil {
panic(err)
}
return messages
}
func GetPaginationMessages(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Message {
messages := []*Message{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&messages)
if err != nil {
panic(err)
}
return messages
}
func getMessage(owner string, name string) *Message {
if owner == "" || name == "" {
return nil
}
message := Message{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&message)
if err != nil {
panic(err)
}
if existed {
return &message
} else {
return nil
}
}
func GetMessage(id string) *Message {
owner, name := util.GetOwnerAndNameFromId(id)
return getMessage(owner, name)
}
func UpdateMessage(id string, message *Message) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getMessage(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(message)
if err != nil {
panic(err)
}
return affected != 0
}
func AddMessage(message *Message) bool {
affected, err := adapter.Engine.Insert(message)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteMessage(message *Message) bool {
affected, err := adapter.Engine.ID(core.PK{message.Owner, message.Name}).Delete(&Message{})
if err != nil {
panic(err)
}
return affected != 0
}
func (p *Message) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

View File

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

View File

@@ -256,7 +256,11 @@ func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
}
}
pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
pProvider, err := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, p.ClientId2)
if err != nil {
return nil, cert, err
}
if pProvider == nil {
return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
}

View File

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

View File

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

View File

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

102
pp/wechatpay.go Normal file
View File

@@ -0,0 +1,102 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pp
import (
"context"
"fmt"
"net/http"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
)
type WechatPaymentProvider struct {
ClientV3 *wechat.ClientV3
appId string
}
func NewWechatPaymentProvider(appId string, mchId string, mchCertSerialNumber string, apiV3Key string, privateKey string) (*WechatPaymentProvider, error) {
pp := &WechatPaymentProvider{appId: appId}
clientV3, err := wechat.NewClientV3(mchId, mchCertSerialNumber, apiV3Key, privateKey)
if err != nil {
return nil, err
}
err = clientV3.AutoVerifySign()
if err != nil {
return nil, err
}
pp.ClientV3 = clientV3
return pp, nil
}
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
bm.Set("providerName", providerName)
bm.Set("productName", productName)
bm.Set("return_url", returnUrl)
bm.Set("notify_url", notifyUrl)
bm.Set("body", productDisplayName)
bm.Set("out_trade_no", paymentName)
bm.Set("total_fee", getPriceString(price))
wechatRsp, err := pp.ClientV3.V3TransactionJsapi(context.Background(), bm)
if err != nil {
return "", err
}
payUrl := fmt.Sprintf("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect", pp.appId, wechatRsp.Response.PrepayId)
return payUrl, nil
}
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
bm, err := wechat.V3ParseNotifyToBodyMap(request)
if err != nil {
return "", "", 0, "", "", err
}
providerName := bm.Get("providerName")
productName := bm.Get("productName")
productDisplayName := bm.Get("body")
paymentName := bm.Get("out_trade_no")
price := util.ParseFloat(bm.Get("total_fee"))
notifyReq, err := wechat.V3ParseNotify(request)
if err != nil {
panic(err)
}
cert := pp.ClientV3.WxPublicKey()
err = notifyReq.VerifySignByPK(cert)
if err != nil {
return "", "", 0, "", "", err
}
return productDisplayName, paymentName, price, productName, providerName, nil
}
func (pp *WechatPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
return "", nil
}

View File

@@ -115,6 +115,7 @@ func initAPI() {
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone")
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/verify-code", &controllers.ApiController{}, "POST:VerifyCode")
beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha")
@@ -188,6 +189,18 @@ func initAPI() {
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/get-chats", &controllers.ApiController{}, "GET:GetChats")
beego.Router("/api/get-chat", &controllers.ApiController{}, "GET:GetChat")
beego.Router("/api/update-chat", &controllers.ApiController{}, "POST:UpdateChat")
beego.Router("/api/add-chat", &controllers.ApiController{}, "POST:AddChat")
beego.Router("/api/delete-chat", &controllers.ApiController{}, "POST:DeleteChat")
beego.Router("/api/get-messages", &controllers.ApiController{}, "GET:GetMessages")
beego.Router("/api/get-message", &controllers.ApiController{}, "GET:GetMessage")
beego.Router("/api/update-message", &controllers.ApiController{}, "POST:UpdateMessage")
beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage")
beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage")
beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts")
beego.Router("/api/get-product", &controllers.ApiController{}, "GET:GetProduct")
beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")

View File

@@ -95,6 +95,15 @@ func GetOwnerAndNameFromId(id string) (string, string) {
return tokens[0], tokens[1]
}
func GetOwnerFromId(id string) string {
tokens := strings.Split(id, "/")
if len(tokens) != 2 {
panic(errors.New("GetOwnerAndNameFromId() error, wrong token count for ID: " + id))
}
return tokens[0]
}
func GetOwnerAndNameFromIdNoCheck(id string) (string, string) {
tokens := strings.SplitN(id, "/", 2)
return tokens[0], tokens[1]

View File

@@ -59,7 +59,7 @@ func getMemoryUsage() (uint64, uint64, error) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.TotalAlloc, virtualMem.Total, nil
return m.Alloc, virtualMem.Total, nil
}
func GetSystemInfo() (*SystemInfo, error) {

View File

@@ -44,6 +44,10 @@ import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import ChatEditPage from "./ChatEditPage";
import ChatListPage from "./ChatListPage";
import MessageEditPage from "./MessageEditPage";
import MessageListPage from "./MessageListPage";
import ProductListPage from "./ProductListPage";
import ProductEditPage from "./ProductEditPage";
import ProductBuyPage from "./ProductBuyPage";
@@ -135,16 +139,22 @@ class App extends Component {
this.setState({selectedMenuKey: "/applications"});
} else if (uri.includes("/resources")) {
this.setState({selectedMenuKey: "/resources"});
} else if (uri.includes("/tokens")) {
this.setState({selectedMenuKey: "/tokens"});
} else if (uri.includes("/records")) {
this.setState({selectedMenuKey: "/records"});
} else if (uri.includes("/tokens")) {
this.setState({selectedMenuKey: "/tokens"});
} else if (uri.includes("/sessions")) {
this.setState({selectedMenuKey: "/sessions"});
} else if (uri.includes("/webhooks")) {
this.setState({selectedMenuKey: "/webhooks"});
} else if (uri.includes("/syncers")) {
this.setState({selectedMenuKey: "/syncers"});
} else if (uri.includes("/certs")) {
this.setState({selectedMenuKey: "/certs"});
} else if (uri.includes("/chats")) {
this.setState({selectedMenuKey: "/chats"});
} else if (uri.includes("/messages")) {
this.setState({selectedMenuKey: "/messages"});
} else if (uri.includes("/products")) {
this.setState({selectedMenuKey: "/products"});
} else if (uri.includes("/payments")) {
@@ -413,6 +423,14 @@ class App extends Component {
"/providers"
));
res.push(Setting.getItem(<Link to="/chats">{i18next.t("general:Chats")}</Link>,
"/chats"
));
res.push(Setting.getItem(<Link to="/messages">{i18next.t("general:Messages")}</Link>,
"/messages"
));
res.push(Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>,
"/resources"
));
@@ -527,6 +545,10 @@ class App extends Component {
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
<Route exact path="/chats" render={(props) => this.renderLoginIfNotLoggedIn(<ChatListPage account={this.state.account} {...props} />)} />
<Route exact path="/chats/:chatName" render={(props) => this.renderLoginIfNotLoggedIn(<ChatEditPage account={this.state.account} {...props} />)} />
<Route exact path="/messages" render={(props) => this.renderLoginIfNotLoggedIn(<MessageListPage account={this.state.account} {...props} />)} />
<Route exact path="/messages/:messageName" render={(props) => this.renderLoginIfNotLoggedIn(<MessageEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />

View File

@@ -867,6 +867,8 @@ class ApplicationEditPage extends React.Component {
submitApplicationEdit(willExist) {
const application = Setting.deepCopy(this.state.application);
application.providers = application.providers?.filter(provider => this.state.providers.map(provider => provider.name).includes(provider.name));
ApplicationBackend.updateApplication("admin", this.state.applicationName, application)
.then((res) => {
if (res.status === "ok") {

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

@@ -0,0 +1,217 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select} from "antd";
import * as ChatBackend from "./backend/ChatBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
class ChatEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
chatName: props.match.params.chatName,
chat: null,
organizations: [],
users: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getChat();
this.getOrganizations();
}
getChat() {
ChatBackend.getChat("admin", this.state.chatName)
.then((chat) => {
this.setState({
chat: chat,
});
this.getUsers(chat.organization);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
this.setState({
users: res,
});
});
}
parseChatField(key, value) {
if ([].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateChatField(key, value) {
value = this.parseChatField(key, value);
const chat = this.state.chat;
chat[key] = value;
this.setState({
chat: chat,
});
}
renderChat() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("chat:New Chat") : i18next.t("chat:Edit Chat")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitChatEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitChatEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteChat()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.chat.organization} onChange={(value => {this.updateChatField("organization", value);})}
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.chat.name} onChange={e => {
this.updateChatField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.chat.displayName} onChange={e => {
this.updateChatField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("chat:User1"), i18next.t("general:User1 - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.chat.user1} onChange={(value => {this.updateChatField("user1", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("chat:User2"), i18next.t("general:User2 - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.chat.user2} onChange={(value => {this.updateChatField("user2", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("chat:Sub users"), i18next.t("chat:Sub users - Tooltip"))} :
</Col>
<Col span={22} >
<Select mode="tags" style={{width: "100%"}} value={this.state.chat.users}
onChange={(value => {this.updateChatField("users", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
/>
</Col>
</Row>
</Card>
);
}
submitChatEdit(willExist) {
const chat = Setting.deepCopy(this.state.chat);
ChatBackend.updateChat(this.state.chat.owner, this.state.chatName, chat)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
chatName: this.state.chat.name,
});
if (willExist) {
this.props.history.push("/chats");
} else {
this.props.history.push(`/chats/${this.state.chat.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateChatField("name", this.state.chatName);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteChat() {
ChatBackend.deleteChat(this.state.chat)
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/chats");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
render() {
return (
<div>
{
this.state.chat !== null ? this.renderChat() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitChatEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitChatEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteChat()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default ChatEditPage;

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

@@ -0,0 +1,268 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as ChatBackend from "./backend/ChatBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
class ChatListPage extends BaseListPage {
newChat() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.applicationName,
name: `chat_${randomName}`,
createdTime: moment().format(),
updatedTime: moment().format(),
organization: this.props.account.owner,
displayName: `New Chat - ${randomName}`,
user1: `${this.props.account.owner}/${this.props.account.name}`,
user2: "",
users: [`${this.props.account.owner}/${this.props.account.name}`],
messageCount: 0,
};
}
addChat() {
const newChat = this.newChat();
ChatBackend.addChat(newChat)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/chats/${newChat.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteChat(i) {
ChatBackend.deleteChat(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
renderTable(chats) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "150px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/chats/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Updated time"),
dataIndex: "updatedTime",
key: "updatedTime",
width: "15 0px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("chat:User1"),
dataIndex: "user1",
key: "user1",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("user1"),
render: (text, record, index) => {
return (
<Link to={`/users/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("chat:User2"),
dataIndex: "user2",
key: "user2",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("user2"),
render: (text, record, index) => {
return (
<Link to={`/users/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Users"),
dataIndex: "users",
key: "users",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("users"),
render: (text, record, index) => {
return Setting.getTags(text, "users");
},
},
{
title: i18next.t("chat:Message count"),
dataIndex: "messageCount",
key: "messageCount",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("messageCount"),
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/chats/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteChat(index)}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={chats} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Chats")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addChat.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
ChatBackend.getChats("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
loading: false,
isAuthorized: false,
});
}
}
});
};
}
export default ChatListPage;

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

@@ -0,0 +1,220 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select} from "antd";
import * as ChatBackend from "./backend/ChatBackend";
import * as MessageBackend from "./backend/MessageBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
const {TextArea} = Input;
class MessageEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
messageName: props.match.params.messageName,
message: null,
organizations: [],
chats: [],
users: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getMessage();
this.getOrganizations();
this.getChats();
}
getMessage() {
MessageBackend.getMessage("admin", this.state.messageName)
.then((message) => {
this.setState({
message: message,
});
this.getUsers(message.organization);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getChats() {
ChatBackend.getChats("admin")
.then((res) => {
this.setState({
chats: (res.msg === undefined) ? res : [],
});
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
this.setState({
users: res,
});
});
}
parseMessageField(key, value) {
if ([].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateMessageField(key, value) {
value = this.parseMessageField(key, value);
const message = this.state.message;
message[key] = value;
this.setState({
message: message,
});
}
renderMessage() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("message:New Message") : i18next.t("message:Edit Message")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitMessageEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitMessageEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteMessage()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.message.organization} onChange={(value => {this.updateMessageField("organization", value);})}
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.message.name} onChange={e => {
this.updateMessageField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("message:Chat"), i18next.t("message:Chat - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.message.chat} onChange={(value => {this.updateMessageField("chat", value);})}
options={this.state.chats.map((chat) => Setting.getOption(chat.name, chat.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("message:Author"), i18next.t("general:Author - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.message.author} onChange={(value => {this.updateMessageField("author", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("message:Text"), i18next.t("message:Text - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={10} value={this.state.message.text} onChange={e => {
this.updateMessageField("text", e.target.value);
}} />
</Col>
</Row>
</Card>
);
}
submitMessageEdit(willExist) {
const message = Setting.deepCopy(this.state.message);
MessageBackend.updateMessage(this.state.message.owner, this.state.messageName, message)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
messageName: this.state.message.name,
});
if (willExist) {
this.props.history.push("/messages");
} else {
this.props.history.push(`/messages/${this.state.message.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateMessageField("name", this.state.messageName);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteMessage() {
MessageBackend.deleteMessage(this.state.message)
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/messages");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
render() {
return (
<div>
{
this.state.message !== null ? this.renderMessage() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitMessageEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitMessageEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteMessage()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default MessageEditPage;

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

@@ -0,0 +1,236 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as MessageBackend from "./backend/MessageBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./PopconfirmModal";
class MessageListPage extends BaseListPage {
newMessage() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.messagename,
name: `message_${randomName}`,
createdTime: moment().format(),
organization: this.props.account.owner,
chat: "",
author: `${this.props.account.owner}/${this.props.account.name}`,
text: "",
};
}
addMessage() {
const newMessage = this.newMessage();
MessageBackend.addMessage(newMessage)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/messages/${newMessage.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteMessage(i) {
MessageBackend.deleteMessage(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
renderTable(messages) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "150px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/messages/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("message:Chat"),
dataIndex: "chat",
key: "chat",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("chat"),
render: (text, record, index) => {
return (
<Link to={`/chats/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("message:Author"),
dataIndex: "author",
key: "author",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("author"),
render: (text, record, index) => {
return (
<Link to={`/users/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("message:Text"),
dataIndex: "text",
key: "text",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("text"),
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/messages/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteMessage(index)}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={messages} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Messages")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addMessage.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
MessageBackend.getMessages("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
loading: false,
isAuthorized: false,
});
}
}
});
};
}
export default MessageListPage;

View File

@@ -139,7 +139,7 @@ class PermissionListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("users"),
render: (text, record, index) => {
return Setting.getTags(text);
return Setting.getTags(text, "users");
},
},
{
@@ -150,7 +150,7 @@ class PermissionListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("roles"),
render: (text, record, index) => {
return Setting.getTags(text);
return Setting.getTags(text, "roles");
},
},
{

View File

@@ -352,7 +352,7 @@ class ProviderEditPage extends React.Component {
[
{id: "Normal", name: i18next.t("provider:Normal")},
{id: "Silent", name: i18next.t("provider:Silent")},
].map((method, index) => <Option key={index} value={method.name}>{method.name}</Option>)
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
@@ -461,13 +461,15 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" ? null : (
this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" ? null : (
<React.Fragment>
<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"))
: Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))}
: this.state.provider.type === "WeChat Pay"
? Setting.getLabel("appId", "appId")
: Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId2} onChange={e => {
@@ -475,18 +477,22 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</Row>
<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"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret2} onChange={e => {
this.updateProviderField("clientSecret2", e.target.value);
}} />
</Col>
</Row>
{
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"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret2} onChange={e => {
this.updateProviderField("clientSecret2", e.target.value);
}} />
</Col>
</Row>
)
}
</React.Fragment>
)
}

View File

@@ -130,7 +130,7 @@ class RoleListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("users"),
render: (text, record, index) => {
return Setting.getTags(text);
return Setting.getTags(text, "users");
},
},
{
@@ -141,7 +141,7 @@ class RoleListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("roles"),
render: (text, record, index) => {
return Setting.getTags(text);
return Setting.getTags(text, "roles");
},
},
{

View File

@@ -42,7 +42,7 @@ export const Countries = [{label: "English", key: "en", country: "US", alt: "Eng
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
{label: "TiếngViệt", key: "vi", country: "VI", alt: "TiếngViệt"},
{label: "TiếngViệt", key: "vn", country: "VN", alt: "TiếngViệt"},
];
export function getThemeData(organization, application) {
@@ -1070,18 +1070,28 @@ export function getTagColor(s) {
return "processing";
}
export function getTags(tags) {
export function getTags(tags, urlPrefix = null) {
const res = [];
if (!tags) {
return res;
}
tags.forEach((tag, i) => {
res.push(
<Tag color={getTagColor(tag)}>
{tag}
</Tag>
);
if (urlPrefix === null) {
res.push(
<Tag color={getTagColor(tag)}>
{tag}
</Tag>
);
} else {
res.push(
<Link to={`/${urlPrefix}/${tag}`}>
<Tag color={getTagColor(tag)}>
{tag}
</Tag>
</Link>
);
}
});
return res;
}

View File

@@ -45,7 +45,7 @@ class SystemInfo extends React.Component {
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);
});
}, 1000 * 3);
}, 1000 * 2);
this.setState({intervalId: id});
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);

View File

@@ -614,7 +614,7 @@ class UserEditPage extends React.Component {
);
}
submitUserEdit(willExist) {
submitUserEdit(needExit) {
const user = Setting.deepCopy(this.state.user);
UserBackend.updateUser(this.state.organizationName, this.state.userName, user)
.then((res) => {
@@ -626,13 +626,18 @@ class UserEditPage extends React.Component {
});
if (this.props.history !== undefined) {
if (willExist) {
this.props.history.push("/users");
if (needExit) {
const userListUrl = sessionStorage.getItem("userListUrl");
if (userListUrl !== null) {
this.props.history.push(userListUrl);
} else {
this.props.history.push("/users");
}
} else {
this.props.history.push(`/users/${this.state.user.owner}/${this.state.user.name}`);
}
} else {
if (willExist) {
if (needExit) {
if (this.state.returnUrl) {
window.location.href = this.state.returnUrl;
}

View File

@@ -70,6 +70,7 @@ class UserListPage extends BaseListPage {
UserBackend.addUser(newUser)
.then((res) => {
if (res.status === "ok") {
sessionStorage.setItem("userListUrl", window.location.pathname);
this.props.history.push({pathname: `/users/${newUser.owner}/${newUser.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
@@ -341,7 +342,10 @@ class UserListPage extends BaseListPage {
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name);
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/users/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => {
sessionStorage.setItem("userListUrl", window.location.pathname);
this.props.history.push(`/users/${record.owner}/${record.name}`);
}}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteUser(index)}
@@ -402,6 +406,8 @@ class UserListPage extends BaseListPage {
const users = res.data;
if (users.length > 0) {
this.getOrganization(users[0].owner);
} else {
this.getOrganization(this.state.organizationName);
}
} else {
if (Setting.isResponseDenied(res)) {
@@ -430,6 +436,8 @@ class UserListPage extends BaseListPage {
const users = res.data;
if (users.length > 0) {
this.getOrganization(users[0].owner);
} else {
this.getOrganization(this.state.organizationName);
}
} else {
if (Setting.isResponseDenied(res)) {

View File

@@ -33,9 +33,8 @@ class ForgetPage extends React.Component {
classes: props,
applicationName: props.applicationName ?? props.match.params?.applicationName,
msg: null,
userId: "",
username: "",
name: "",
username: "",
phone: "",
email: "",
dest: "",
@@ -86,7 +85,7 @@ class ForgetPage extends React.Component {
const phone = res.data.phone;
const email = res.data.email;
if (phone === "" && email === "") {
if (!phone && !email) {
Setting.showMessage("error", "no verification method!");
} else {
this.setState({
@@ -124,18 +123,16 @@ class ForgetPage extends React.Component {
});
break;
case "step2":
const oAuthParams = Util.getOAuthGetParameters();
AuthBackend.login({
UserBackend.verifyCode({
application: forms.step2.getFieldValue("application"),
organization: forms.step2.getFieldValue("organization"),
username: forms.step2.getFieldValue("dest"),
name: this.state.name,
code: forms.step2.getFieldValue("code"),
type: "login",
}, oAuthParams).then(res => {
}).then(res => {
if (res.status === "ok") {
this.setState({current: 2, userId: res.data});
this.setState({current: 2, code: forms.step2.getFieldValue("code")});
} else {
Setting.showMessage("error", res.msg);
}
@@ -150,7 +147,7 @@ class ForgetPage extends React.Component {
onFinish(values) {
values.username = this.state.name;
values.userOwner = this.getApplicationObj()?.organizationObj.name;
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword).then(res => {
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword, this.state.code).then(res => {
if (res.status === "ok") {
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
} else {
@@ -387,7 +384,6 @@ class ForgetPage extends React.Component {
hasFeedback
>
<Input.Password
disabled={this.state.userId === ""}
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
@@ -414,14 +410,13 @@ class ForgetPage extends React.Component {
]}
>
<Input.Password
disabled={this.state.userId === ""}
prefix={<CheckCircleOutlined />}
placeholder={i18next.t("signup:Confirm")}
/>
</Form.Item>
<br />
<Form.Item hidden={this.state.current !== 2}>
<Button block type="primary" htmlType="submit" disabled={this.state.userId === ""}>
<Button block type="primary" htmlType="submit">
{i18next.t("forget:Change Password")}
</Button>
</Form.Item>

View File

@@ -55,11 +55,10 @@ export function getUserApplication(owner, name) {
}
export function updateApplication(owner, name, application) {
const newApplication = Setting.deepCopy(application);
return fetch(`${Setting.ServerUrl}/api/update-application?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newApplication),
body: JSON.stringify(application),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},

View File

@@ -0,0 +1,71 @@
// 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 getChats(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-chats?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getChat(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-chat?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateChat(owner, name, chat) {
const newChat = Setting.deepCopy(chat);
return fetch(`${Setting.ServerUrl}/api/update-chat?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newChat),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function addChat(chat) {
const newChat = Setting.deepCopy(chat);
return fetch(`${Setting.ServerUrl}/api/add-chat`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newChat),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function deleteChat(chat) {
const newChat = Setting.deepCopy(chat);
return fetch(`${Setting.ServerUrl}/api/delete-chat`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newChat),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -0,0 +1,71 @@
// 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 getMessages(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-messages?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getMessage(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-message?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateMessage(owner, name, message) {
const newMessage = Setting.deepCopy(message);
return fetch(`${Setting.ServerUrl}/api/update-message?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newMessage),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function addMessage(message) {
const newMessage = Setting.deepCopy(message);
return fetch(`${Setting.ServerUrl}/api/add-message`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newMessage),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function deleteMessage(message) {
const newMessage = Setting.deepCopy(message);
return fetch(`${Setting.ServerUrl}/api/delete-message`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newMessage),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -93,12 +93,16 @@ export function getAffiliationOptions(url, code) {
}).then(res => res.json());
}
export function setPassword(userOwner, userName, oldPassword, newPassword) {
export function setPassword(userOwner, userName, oldPassword, newPassword, code = "") {
const formData = new FormData();
formData.append("userOwner", userOwner);
formData.append("userName", userName);
formData.append("oldPassword", oldPassword);
formData.append("newPassword", newPassword);
if (code) {
formData.append("code", code);
}
return fetch(`${Setting.ServerUrl}/api/set-password`, {
method: "POST",
credentials: "include",
@@ -188,3 +192,14 @@ export function getCaptcha(owner, name, isCurrentProvider) {
},
}).then(res => res.json()).then(res => res.data);
}
export function verifyCode(values) {
return fetch(`${Setting.ServerUrl}/api/verify-code`, {
method: "POST",
credentials: "include",
body: JSON.stringify(values),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -21,7 +21,7 @@ export const AgreementModal = (props) => {
const [doc, setDoc] = useState("");
useEffect(() => {
getTermsOfUseContent(application.termsOfUseUrl).then((data) => {
getTermsOfUseContent(application.termsOfUse).then((data) => {
setDoc(data);
});
}, []);

View File

@@ -22,7 +22,7 @@ import id from "./locales/id/data.json";
import ja from "./locales/ja/data.json";
import ko from "./locales/ko/data.json";
import ru from "./locales/ru/data.json";
import vi from "./locales/vi/data.json";
import vn from "./locales/vn/data.json";
import * as Conf from "./Conf";
import {initReactI18next} from "react-i18next";
@@ -36,7 +36,7 @@ const resources = {
ja: ja,
ko: ko,
ru: ru,
vi: vi,
vn: vn,
};
function initLanguage() {
@@ -80,8 +80,8 @@ function initLanguage() {
case "ru":
language = "ru";
break;
case "vi":
language = "vi";
case "vn":
language = "vn";
break;
default:
language = Conf.DefaultLanguage;

View File

@@ -1,119 +1,119 @@
{
"account": {
"Logout": "Abmeldung",
"Logout": "Abmelden",
"My Account": "Mein Konto",
"Sign Up": "Anmeldung"
"Sign Up": "Anmelden"
},
"adapter": {
"Duplicated policy rules": "Doppelte Richtlinienregeln",
"Edit Adapter": "Bearbeiten Sie den Adapter",
"Failed to sync policies": "Fehler beim Synchronisieren von Richtlinien",
"Edit Adapter": "Adapter bearbeiten",
"Failed to sync policies": "Fehler beim Synchronisieren der Richtlinien",
"New Adapter": "Neuer Adapter",
"Policies": "Richtlinien",
"Policies - Tooltip": "Casbin-Richtlinienregeln",
"Sync policies successfully": "Synchronisierungspolitiken erfolgreich umgesetzt"
"Policies - Tooltip": "Casbin Richtlinienregeln",
"Sync policies successfully": "Richtlinien synchronisiert"
},
"application": {
"Always": "Immer",
"Auto signin": "Automatische Anmeldung",
"Auto signin - Tooltip": "Wenn eine angemeldete Sitzung in Casdoor vorhanden ist, wird sie automatisch für die Anmeldung auf Anwendungsebene verwendet",
"Background URL": "Hintergrund-URL",
"Background URL - Tooltip": "URL des Hintergrundbildes, das auf der Anmeldeseite verwendet wird",
"Auto signin - Tooltip": "Wenn eine angemeldete Session in Casdoor vorhanden ist, wird diese automatisch für die Anmeldung auf Anwendungsebene verwendet",
"Background URL": "Background-URL",
"Background URL - Tooltip": "URL des Hintergrundbildes, das auf der Anmeldeseite angezeigt wird",
"Center": "Zentrum",
"Copy SAML metadata URL": "Kopieren Sie die SAML-Metadaten-URL",
"Copy prompt page URL": "Kopiere die URL der Prompt-Seite",
"Copy signin page URL": "Kopieren Sie die URL der Anmeldeseite",
"Copy signup page URL": "Kopieren Sie die URL der Anmeldeseite",
"Edit Application": "Bearbeitungsanwendung",
"Copy SAML metadata URL": "SAML-Metadaten-URL kopieren",
"Copy prompt page URL": "URL der Prompt-Seite kopieren",
"Copy signin page URL": "URL der Anmeldeseite kopieren",
"Copy signup page URL": "URL der Anmeldeseite kopieren",
"Edit Application": "Anwendung bearbeiten",
"Enable Email linking": "E-Mail-Verknüpfung aktivieren",
"Enable Email linking - Tooltip": "Bei der Verwendung von 3rd-Party-Anbietern zur Anmeldung wird, wenn es in der Organisation einen Benutzer mit der gleichen E-Mail gibt, automatisch die 3rd-Party-Anmelde-Methode mit diesem Benutzer verbunden",
"Enable SAML compression": "Aktivieren Sie SAML-Komprimierung",
"Enable Email linking - Tooltip": "Bei der Verwendung von Drittanbietern zur Anmeldung wird, wenn es in der Organisation einen Benutzer mit der gleichen E-Mail gibt, automatisch die Drittanbieter-Anmelde-Methode mit diesem Benutzer verbunden",
"Enable SAML compression": "SAML-Komprimierung aktivieren",
"Enable SAML compression - Tooltip": "Ob SAML-Antwortnachrichten komprimiert werden sollen, wenn Casdoor als SAML-IdP verwendet wird",
"Enable WebAuthn signin": "Aktivieren Sie die Anmeldung mit WebAuthn",
"Enable WebAuthn signin": "Anmeldung mit WebAuthn aktivieren",
"Enable WebAuthn signin - Tooltip": "Ob Benutzern erlaubt werden soll, sich mit WebAuthn anzumelden",
"Enable code signin": "Aktivieren Sie die Code-Unterzeichnung",
"Enable code signin - Tooltip": "Ob Benutzern erlaubt werden soll, sich mit einer Telefon- oder E-Mail-Bestätigungscode anzumelden",
"Enable password": "Aktiviere das Passwort",
"Enable code signin": "Code Anmeldung aktivieren",
"Enable code signin - Tooltip": "Ob Benutzern erlaubt werden soll, sich mit einem Telefon- oder E-Mail-Bestätigungscode anzumelden",
"Enable password": "Passwort aktivieren",
"Enable password - Tooltip": "Ob Benutzern erlaubt werden soll, sich mit einem Passwort anzumelden",
"Enable side panel": "Aktiviere die Seitenleiste",
"Enable side panel": "Sidepanel aktivieren",
"Enable signin session - Tooltip": "Ob Casdoor eine Sitzung aufrechterhält, nachdem man sich von der Anwendung aus bei Casdoor angemeldet hat",
"Enable signup": "Aktivieren Sie die Registrierung",
"Enable signup": "Registrierung aktivieren",
"Enable signup - Tooltip": "Ob Benutzern erlaubt werden soll, ein neues Konto zu registrieren",
"Failed to sign in": "Fehler bei der Anmeldung",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"First, last": "First, last",
"Follow organization theme": "Folge dem Thema der Organisation",
"Form CSS": "Formular CSS",
"Form CSS - Edit": "Formular CSS - Bearbeiten",
"Form CSS - Tooltip": "CSS-Styling der Anmelde-, Registrierungs- und Passwort-vergessen-Formulare (z. B. Hinzufügen von Rahmen und Schatten)",
"Follow organization theme": "Folge dem Theme der Organisation",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Bearbeiten",
"Form CSS - Tooltip": "CSS-Styling der Anmelde-, Registrierungs- und Passwort-vergessen-Seite (z. B. Hinzufügen von Rahmen und Schatten)",
"Form position": "Formposition",
"Form position - Tooltip": "Ort der Anmelde-, Anmelde- und Passwort vergessen-Formulare",
"Form position - Tooltip": "Position der Anmelde-, Registrierungs- und Passwort-vergessen-Formulare",
"Grant types": "Grant-Typen",
"Grant types - Tooltip": "Wählen Sie aus, welche Förderarten im OAuth-Protokoll zulässig sind",
"Grant types - Tooltip": "Wählen Sie aus, welche Grant-Typen im OAuth-Protokoll zulässig sind",
"Incremental": "Incremental",
"Left": "Links",
"Logged in successfully": "Erfolgreich eingeloggt",
"Logged out successfully": "Erfolgreich ausgeloggt",
"New Application": "Neue Anwendung",
"No verification": "No verification",
"None": "kein",
"None": "kein(e)",
"Normal": "Normal",
"Only signup": "Only signup",
"Please input your application!": "Bitte geben Sie Ihre Bewerbung ein!",
"Please input your application!": "Bitte geben Sie Ihre Anwendung ein!",
"Please input your organization!": "Bitte geben Sie Ihre Organisation ein!",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei aus",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Die URL der Seite wurde erfolgreich in die Zwischenablage kopiert. Bitte fügen Sie sie in das Inkognito-Fenster oder einen anderen Browser ein",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Die URL der Seite wurde erfolgreich in die Zwischenablage kopiert. Bitte fügen Sie sie in einen Inkognito-Tab oder einen anderen Browser ein",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Weiterleitungs-URL",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Weiterleitungs-URL (Assertion Consumer Service POST Binding URL)",
"Redirect URLs": "Weiterleitungs-URLs",
"Redirect URLs - Tooltip": "Liste erlaubter Umleitungs-URLs mit Unterstützung von regulärer Ausdrucksprüfung; URLs, die nicht in der Liste enthalten sind, können nicht umgeleitet werden",
"Refresh token expire": "Das Auffrischen des Tokens ist abgelaufen",
"Refresh token expire - Tooltip": "Ablaufzeit des Aktualisierungstokens",
"Refresh token expire": "Gültigkeitsdauer des Refresh-Tokens",
"Refresh token expire - Tooltip": "Angabe der Gültigkeitsdauer des Refresh Tokens",
"Right": "Rechts",
"Rule": "Regel",
"SAML metadata": "SAML-Metadaten",
"SAML metadata - Tooltip": "Die Metadaten des SAML-Protokolls",
"SAML metadata URL copied to clipboard successfully": "SAML-Metadaten URL erfolgreich in die Zwischenablage kopiert",
"SAML reply URL": "SAML Antwort-URL",
"Side panel HTML": "Seitenleisten-HTML",
"Side panel HTML - Edit": "Seitenleisten HTML - Bearbeiten",
"Side panel HTML - Tooltip": "Passen Sie den HTML-Code für das Seitenfeld der Login-Seite an",
"SAML reply URL": "SAML Reply-URL",
"Side panel HTML": "Sidepanel-HTML",
"Side panel HTML - Edit": "Sidepanel HTML - Bearbeiten",
"Side panel HTML - Tooltip": "Passen Sie den HTML-Code für das Sidepanel der Login-Seite an",
"Sign Up Error": "Registrierungsfehler",
"Signin": "Signin",
"Signin (Default True)": "Signin (Default True)",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Die URL der Anmeldeseite wurde erfolgreich in die Zwischenablage kopiert. Bitte fügen Sie sie in das Inkognito-Fenster oder einen anderen Browser ein",
"Signin session": "Anmeldesitzung",
"Signup items": "Anmeldungsartikel",
"Signup items - Tooltip": "Gegenstände, die Benutzer ausfüllen müssen, wenn sie neue Konten registrieren",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Anmeldeseite-URL erfolgreich in die Zwischenablage kopiert. Bitte fügen Sie sie in das Inkognito-Fenster oder einen anderen Browser ein",
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, ein neues Konto anzumelden",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Die URL der Anmeldeseite wurde in die Zwischenablage kopiert. Bitte fügen Sie sie in einen Inkognito-Tab oder einen anderen Browser ein",
"Signin session": "Anmeldesession",
"Signup items": "Registrierungs Items",
"Signup items - Tooltip": "Items, die Benutzer ausfüllen müssen, wenn sie neue Konten registrieren",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Die URL der Registrierungsseite wurde in die Zwischenablage kopiert. Bitte fügen Sie sie in einen Inkognito-Tab oder einen anderen Browser ein",
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, ein neues Konto zu registrieren",
"Token expire": "Token läuft ab",
"Token expire - Tooltip": "Ablaufzeit des Zugriffstokens",
"Token expire - Tooltip": "Ablaufzeit des Access-Tokens",
"Token format": "Token-Format",
"Token format - Tooltip": "Das Format des Zugriffstokens",
"You are unexpected to see this prompt page": "Du bist unerwartet diese Aufforderungsseite zu sehen"
"Token format - Tooltip": "Das Format des Access-Tokens",
"You are unexpected to see this prompt page": "Sie sind unerwartet auf diese Aufforderungsseite gelangt"
},
"cert": {
"Bit size": "Bitgröße",
"Bit size - Tooltip": "Geheimschlüssellänge",
"Bit size - Tooltip": "Länge des Secret-Keys",
"Certificate": "Zertifikat",
"Certificate - Tooltip": "Öffentliches Schlüsselzertifikat, das zum Entschlüsseln der JWT-Signatur des Access Tokens verwendet wird. Dieses Zertifikat muss normalerweise auf der Casdoor SDK-Seite (d. h. der Anwendung) bereitgestellt werden, um das JWT zu analysieren",
"Certificate copied to clipboard successfully": "Zertifikat erfolgreich in die Zwischenablage kopiert",
"Certificate - Tooltip": "Public-Key-Zertifikat, das zum Entschlüsseln der JWT-Signatur des Access Tokens verwendet wird. Dieses Zertifikat muss normalerweise auf der Casdoor SDK-Seite (d. h. der Anwendung) bereitgestellt werden, um das JWT zu parsen",
"Certificate copied to clipboard successfully": "Zertifikat in die Zwischenablage kopiert",
"Copy certificate": "Kopieren Sie das Zertifikat",
"Copy private key": "Privaten Schlüssel kopieren",
"Copy private key": "Private-Key kopieren",
"Crypto algorithm": "Kryptoalgorithmus",
"Crypto algorithm - Tooltip": "Verschlüsselungsalgorithmus, der vom Zertifikat verwendet wird",
"Download certificate": "Zertifikat herunterladen",
"Download private key": "Privater Schlüssel herunterladen",
"Download private key": "Private-Key herunterladen",
"Edit Cert": "Edit Cert - Zertifikat bearbeiten",
"Expire in years": "Verfallen in Jahren",
"Expire in years": "Ablaufzeit in Jahren",
"Expire in years - Tooltip": "Gültigkeitsdauer des Zertifikats in Jahren",
"New Cert": "Neues Zertifikat",
"Private key": "Privater Schlüssel",
"Private key": "Private-Key",
"Private key - Tooltip": "Privater Schlüssel, der zum öffentlichen Schlüsselzertifikat gehört",
"Private key copied to clipboard successfully": "Privater Schlüssel wurde erfolgreich in die Zwischenablage kopiert",
"Private key copied to clipboard successfully": "Private-Key wurde erfolgreich in die Zwischenablage kopiert",
"Scope - Tooltip": "Nutzungsszenarien des Zertifikats",
"Type - Tooltip": "Art des Zertifikats"
},
@@ -126,7 +126,7 @@
"Please input your phone verification code!": "Bitte geben Sie den Bestätigungscode Ihres Telefons ein!",
"Please input your verification code!": "Bitte geben Sie Ihren Bestätigungscode ein!",
"Send Code": "Code senden",
"Sending": "Senden",
"Sending": "Sendet",
"Submit and complete": "Einreichen und abschließen"
},
"forget": {
@@ -134,7 +134,7 @@
"Change Password": "Passwort ändern",
"Choose email or phone": "Wählen Sie E-Mail oder Telefon",
"Next Step": "Nächster Schritt",
"Please input your username!": "Bitte gib deinen Benutzernamen ein!",
"Please input your username!": "Bitte geben Sie Ihren Benutzernamen ein!",
"Reset": "Zurücksetzen",
"Retrieve password": "Passwort abrufen",
"Unknown forget type": "Unbekannter Vergesslichkeitstyp",
@@ -146,24 +146,24 @@
"Adapter - Tooltip": "Tabellenname des Policy Stores",
"Adapters": "Adapter",
"Add": "Hinzufügen",
"Affiliation URL": "Zugehörigkeits-URL",
"Affiliation URL": "Affiliation-URL",
"Affiliation URL - Tooltip": "Die Homepage-URL für die Zugehörigkeit",
"Application": "Bewerbung",
"Application": "Applikation",
"Applications": "Anwendungen",
"Applications that require authentication": "Anwendungen, die eine Authentifizierung erfordern",
"Avatar": "Avatar",
"Avatar - Tooltip": "Öffentliches Avatarbild für den Benutzer",
"Back Home": "Zurück zu Hause",
"Back Home": "Zurück nach Hause",
"Cancel": "Abbrechen",
"Captcha": "Captcha",
"Cert": "Zertifikat",
"Cert - Tooltip": "Das öffentliche Schlüsselzertifikat, das vom Client-SDK, das mit dieser Anwendung korrespondiert, verifiziert werden muss",
"Cert - Tooltip": "Das Public-Key-Zertifikat, das vom Client-SDK, das mit dieser Anwendung korrespondiert, verifiziert werden muss",
"Certs": "Zertifikate",
"Click to Upload": "Klicken Sie zum Hochladen",
"Client IP": "Kunden-IP",
"Client IP": "Client-IP",
"Close": "Schließen",
"Created time": "Erstellte Zeit",
"Default application": "Standardanwendung",
"Default application": "Standard Anwendung",
"Default application - Tooltip": "Standard-Anwendung für Benutzer, die direkt von der Organisationsseite registriert wurden",
"Default avatar": "Standard-Avatar",
"Default avatar - Tooltip": "Standard-Avatar, der verwendet wird, wenn neu registrierte Benutzer kein Avatar-Bild festlegen",
@@ -176,16 +176,16 @@
"Edit": "Bearbeiten",
"Email": "E-Mail",
"Email - Tooltip": "Gültige E-Mail-Adresse",
"Failed to add": "Konnte nicht hinzufügen",
"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 save": "Konnte nicht gespeichert werden",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon Icon-URL, die auf allen Casdoor-Seiten der Organisation verwendet wird",
"Favicon - Tooltip": "Favicon-URL, die auf allen Casdoor-Seiten der Organisation verwendet wird",
"First name": "Vorname",
"Forget URL": "Vergessen Sie die URL",
"Forget URL": "Passwort vergessen URL",
"Forget URL - Tooltip": "Benutzerdefinierte URL für die \"Passwort vergessen\" Seite. Wenn nicht festgelegt, wird die standardmäßige Casdoor \"Passwort vergessen\" Seite verwendet. Wenn sie festgelegt ist, wird der \"Passwort vergessen\" Link auf der Login-Seite zu dieser URL umgeleitet",
"Found some texts still not translated? Please help us translate at": "Hast du noch einige Texte gefunden, die nicht übersetzt wurden? Bitte hilf uns beim Übersetzen",
"Found some texts still not translated? Please help us translate at": "Haben Sie noch Texte gefunden, die nicht übersetzt wurden? Bitte helfen Sie uns beim Übersetzen",
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
"Home": "Zuhause",
"Home - Tooltip": "Homepage der Anwendung",
@@ -216,22 +216,22 @@
"Organizations": "Organisationen",
"Password": "Passwort",
"Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist",
"Password salt": "Passworthash",
"Password salt": "Passwort-Salt",
"Password salt - Tooltip": "Zufälliger Parameter, der für die Verschlüsselung von Passwörtern verwendet wird",
"Password type": "Passworttyp",
"Password type - Tooltip": "Speicherformat von Passwörtern in der Datenbank",
"Payments": "Zahlungen",
"Permissions": "Erlaubnisse",
"Permissions": "Rechte",
"Permissions - Tooltip": "Berechtigungen, die diesem Benutzer gehören",
"Phone": "Telefon",
"Phone - Tooltip": "Telefonnummer",
"Preview": "Vorschau",
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
"Products": "Produkte",
"Provider": "Anbieter",
"Provider - Tooltip": "Zahlungsanbieter müssen konfiguriert werden, einschließlich PayPal, Alipay, WeChat Pay usw.",
"Providers": "Anbieter",
"Providers - Tooltip": "Anbieter müssen konfiguriert werden, einschließlich Anmeldung durch Drittanbieter, Objektspeicherung, Verifizierungscode usw.",
"Provider": "Provider",
"Provider - Tooltip": "Zahlungsprovider, die konfiguriert werden müssen, inkl. PayPal, Alipay, WeChat Pay usw.",
"Providers": "Provider",
"Providers - Tooltip": "Provider, die konfiguriert werden müssen, einschließlich Drittanbieter-Logins, Objektspeicherung, Verifizierungscode usw.",
"Real name": "Echter Name",
"Records": "Datensätze",
"Request URI": "Anforderungs-URI",
@@ -239,9 +239,9 @@
"Roles": "Rollen",
"Roles - Tooltip": "Rollen, denen der Benutzer angehört",
"Save": "Speichern",
"Save & Exit": "Speichern und Beenden",
"Session ID": "Sitzungs-ID",
"Sessions": "Sitzungen",
"Save & Exit": "Speichern und verlassen",
"Session ID": "Session-ID",
"Sessions": "Sessions",
"Signin URL": "Anmeldungs-URL",
"Signin URL - Tooltip": "Benutzerdefinierte URL für die Anmeldeseite. Wenn sie nicht festgelegt ist, wird die standardmäßige Casdoor-Anmeldeseite verwendet. Wenn sie festgelegt ist, leiten die Anmeldelinks auf verschiedenen Casdoor-Seiten zu dieser URL um",
"Signup URL": "Anmelde-URL",
@@ -250,7 +250,7 @@
"Signup application - Tooltip": "Durch welche Anwendung hat sich der Benutzer angemeldet, als er sich registriert hat?",
"Sorry, the page you visited does not exist.": "Entschuldigung, die von Ihnen besuchte Seite existiert nicht.",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Entschuldigung, der von Ihnen besuchte Benutzer existiert nicht oder Sie sind nicht autorisiert, auf diesen Benutzer zuzugreifen.",
"Sorry, you do not have permission to access this page or logged in status invalid.": "Es tut uns leid, aber Sie haben keine Berechtigung, auf diese Seite zuzugreifen, oder Ihr angemeldeter Status ist ungültig.",
"Sorry, you do not have permission to access this page or logged in status invalid.": "Es tut uns leid, aber Sie haben keine Berechtigung, auf diese Seite zuzugreifen, oder Sie sind nicht angemeldet.",
"State": "Bundesland / Staat",
"State - Tooltip": "Bundesland",
"Successfully added": "Erfolgreich hinzugefügt",
@@ -258,7 +258,7 @@
"Successfully saved": "Erfolgreich gespeichert",
"Supported country codes": "Unterstützte Ländercodes",
"Supported country codes - Tooltip": "Ländercodes, die von der Organisation unterstützt werden. Diese Codes können als Präfix ausgewählt werden, wenn SMS-Verifizierungscodes gesendet werden",
"Sure to delete": "Sicher löschen (or Gesichertes Löschen)",
"Sure to delete": "Sicher zu löschen",
"Swagger": "Swagger",
"Sync": "Synchronisieren",
"Syncers": "Syncers",
@@ -295,7 +295,7 @@
"Enable SSL - Tooltip": "Ob SSL aktiviert werden soll",
"Group ID": "Gruppen-ID",
"Last Sync": "Letzte Synchronisation",
"Server": "Serverh)",
"Server": "Server",
"Server host": "Server Host",
"Server host - Tooltip": "LDAP-Server-Adresse",
"Server name": "Servername",
@@ -307,7 +307,7 @@
},
"login": {
"Auto sign in": "Automatische Anmeldung",
"Continue with": "Mach weiter mit",
"Continue with": "Weitermachen mit",
"Email or phone": "E-Mail oder Telefon",
"Forgot password?": "Passwort vergessen?",
"Loading": "Laden",
@@ -325,30 +325,30 @@
"Signing in...": "Anmelden...",
"Successfully logged in with WebAuthn credentials": "Erfolgreich mit WebAuthn-Anmeldeinformationen angemeldet",
"The input is not valid Email or phone number!": "Die Eingabe ist keine gültige E-Mail-Adresse oder Telefonnummer!",
"To access": "Zu Zugriff",
"To access": "Zum Zugriff",
"Verification code": "Verifizierungscode",
"WebAuthn": "WebAuthn",
"sign up now": "Melde dich jetzt an",
"username, Email or phone": "Benutzername, E-Mail oder Telefon"
},
"model": {
"Edit Model": "Bearbeiten Modell",
"Edit Model": "Modell bearbeiten",
"Model text": "Modelltext",
"Model text - Tooltip": "Casbin Zugriffskontrollmodell inklusive integrierter Modelle wie ACL, RBAC, ABAC, RESTful, usw. Sie können auch benutzerdefinierte Modelle erstellen. Weitere Informationen finden Sie auf der Casbin-Website",
"New Model": "Neues Modell"
},
"organization": {
"Account items": "Kontenpositionen",
"Account items": "Konto Items",
"Account items - Tooltip": "Elemente auf der persönlichen Einstellungsseite",
"Edit Organization": "Organisation bearbeiten",
"Follow global theme": "Folge dem globalen Thema",
"Init score": "Initiale Punktzahl",
"Follow global theme": "Folge dem globalen Theme",
"Init score": "Initialer Score",
"Init score - Tooltip": "Anfangspunkte, die Benutzern bei der Registrierung vergeben werden",
"Is profile public": "Ist das Profil öffentlich?",
"Is profile public - Tooltip": "Nach der Schließung können nur globale Administratoren oder Benutzer in der gleichen Organisation auf die Profilseite des Benutzers zugreifen",
"Modify rule": "Regel ändern",
"New Organization": "Neue Organisation",
"Soft deletion": "Weiche Löschung",
"Soft deletion": "Softe Löschung",
"Soft deletion - Tooltip": "Wenn aktiviert, werden gelöschte Benutzer nicht vollständig aus der Datenbank entfernt. Stattdessen werden sie als gelöscht markiert",
"Tags": "Tags",
"Tags - Tooltip": "Sammlung von Tags, die für Benutzer zur Auswahl zur Verfügung stehen",
@@ -362,8 +362,8 @@
"Currency": "Währung",
"Currency - Tooltip": "Wie USD, CNY usw.",
"Download Invoice": "Rechnung herunterladen",
"Edit Payment": "Bearbeiten Sie die Zahlung",
"Individual": "Individuum",
"Edit Payment": "Zahlung bearbeiten",
"Individual": "individuell",
"Invoice URL": "Rechnungs-URL",
"Invoice URL - Tooltip": "URL für den Download der Rechnung",
"Invoice actions": "Rechnungsaktionen",
@@ -391,16 +391,16 @@
"Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.": "Bitte prüfen Sie sorgfältig Ihre Rechnungsinformationen. Sobald die Rechnung ausgestellt wurde, kann sie nicht zurückgenommen oder geändert werden.",
"Please click the below button to return to the original website": "Bitte klicken Sie auf den unten stehenden Button, um zur ursprünglichen Website zurückzukehren",
"Please pay the order first!": "Bitte zahlen Sie zuerst die Bestellung!",
"Processing...": "Verarbeitung...",
"Processing...": "In Bearbeitung...",
"Product": "Produkt",
"Product - Tooltip": "Produktname",
"Result": "Ergebnis",
"Return to Website": "Zurück zur Website",
"The payment has failed": "Die Zahlung ist fehlgeschlagen",
"The payment is still under processing": "Die Zahlung wird immer noch bearbeitet",
"Type - Tooltip": "Zahlungsmethode, die beim Kauf des Produkts verwendet wird",
"Type - Tooltip": "Zahlungsmethode, die beim Kauf des Produkts verwendet wurde",
"You have successfully completed the payment": "Sie haben die Zahlung erfolgreich abgeschlossen",
"please wait for a few seconds...": "Bitte warten Sie einige Sekunden...",
"please wait for a few seconds...": "Bitte warten Sie ein paar Sekunden...",
"the current state is": "der aktuelle Zustand ist"
},
"permission": {
@@ -408,17 +408,17 @@
"Actions - Tooltip": "Erlaubte Aktionen",
"Admin": "Admin",
"Allow": "erlauben",
"Approve time": "Zeit genehmigen",
"Approve time": "Zeit der Genehmigung",
"Approve time - Tooltip": "Die Genehmigungszeit für diese Erlaubnis",
"Approved": "Genehmigt",
"Approver": "Genehmiger",
"Approver - Tooltip": "Die Person, die die Genehmigung genehmigt hat",
"Deny": "Leugnen",
"Edit Permission": "Bearbeitungsberechtigung",
"Effect": "Wirkung",
"Effect - Tooltip": "erlauben oder ablehnen",
"Deny": "Ablehnen",
"Edit Permission": "Recht bearbeiten",
"Effect": "Effekt",
"Effect - Tooltip": "Erlauben oder ablehnen",
"New Permission": "Neue Genehmigung",
"Pending": "In Erwartung",
"Pending": "Ausstehend",
"Read": "Lesen",
"Resource type": "Ressourcentyp",
"Resource type - Tooltip": "Art der Ressource",
@@ -442,8 +442,8 @@
"New Product": "Neues Produkt",
"Pay": "Zahlen",
"PayPal": "PayPal",
"Payment providers": "Zahlungsanbieter",
"Payment providers - Tooltip": "Anbieter von Zahlungsdiensten",
"Payment providers": "Zahlungsprovider",
"Payment providers - Tooltip": "Provider von Zahlungsdiensten",
"Placing order...": "Bestellung aufgeben...",
"Please provide your username in the remark": "Bitte geben Sie Ihren Benutzernamen in der Anmerkung an",
"Please scan the QR code to pay": "Bitte scannen Sie den QR-Code, um zu bezahlen",
@@ -451,7 +451,7 @@
"Price - Tooltip": "Preis des Produkts",
"Quantity": "Menge",
"Quantity - Tooltip": "Menge des Produkts",
"Return URL": "Rückgabe-URL",
"Return URL": "Rückkeht-URL",
"Return URL - Tooltip": "URL für die Rückkehr nach einem erfolgreichen Kauf",
"SKU": "SKU",
"Sold": "Verkauft",
@@ -459,82 +459,82 @@
"Tag - Tooltip": "Tag des Produkts",
"Test buy page..": "Testkaufseite.",
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
"This product is currently not in sale.": "Dieses Produkt ist derzeit nicht im Verkauf.",
"This product is currently not in sale.": "Dieses Produkt steht derzeit nicht zum Verkauf.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Zugriffsschlüssel",
"Access key": "Access-Key",
"Access key - Tooltip": "Zugriffsschlüssel",
"Agent ID": "Agenten-ID",
"Agent ID - Tooltip": "Agenten-ID",
"App ID": "App ID",
"App ID - Tooltip": "App-ID",
"App key": "App-Schlüssel",
"App key": "App-Key",
"App key - Tooltip": "App-Schlüssel",
"App secret": "App-Geheimnis",
"App secret": "App-Secret",
"AppSecret - Tooltip": "App-Geheimnis",
"Auth URL": "Auth-URL",
"Auth URL - Tooltip": "Auth-URL",
"Bucket": "Eimer",
"Bucket - Tooltip": "Name des Eimers",
"Bucket": "Bucket",
"Bucket - Tooltip": "Name des Buckets",
"Can not parse metadata": "Kann Metadaten nicht durchsuchen / auswerten",
"Can signin": "Einloggen möglich",
"Can signup": "Kann registrieren",
"Can unlink": "Entkoppeln möglich",
"Can signin": "Kann sich einloggen",
"Can signup": "Kann sich registrieren",
"Can unlink": "Entlinken möglich",
"Category": "Kategorie",
"Category - Tooltip": "Wählen Sie eine Kategorie aus",
"Channel No.": "Kanal Nr.",
"Channel No. - Tooltip": "Kanalnummer.",
"Client ID": "Kunden-ID",
"Client ID - Tooltip": "Kundennummer",
"Client ID 2": "Kunden-ID 2",
"Client ID 2 - Tooltip": "Die zweite Kundennnummer (ID)",
"Client secret": "Kunden-Geheimnis",
"Client secret - Tooltip": "Kunden-Geheimnis",
"Client secret 2": "Kunden-Geheimnis 2",
"Client secret 2 - Tooltip": "Der zweite Geheimcode für den Kunden",
"Client ID": "Client-ID",
"Client ID - Tooltip": "Client-ID",
"Client ID 2": "Client-ID 2",
"Client ID 2 - Tooltip": "Die zweite Client-ID",
"Client secret": "Client-Secret",
"Client secret - Tooltip": "Client-Geheimnis",
"Client secret 2": "Client-Secret 2",
"Client secret 2 - Tooltip": "Der zweite Client-Secret-Key",
"Copy": "Kopieren",
"Disable SSL": "SSL deaktivieren",
"Disable SSL - Tooltip": "Ob die Deaktivierung des SSL-Protokolls bei der Kommunikation mit dem STMP-Server erfolgen soll",
"Domain": "Domain",
"Domain - Tooltip": "Benutzerdefinierte Domäne für Objektspeicher",
"Edit Provider": "Anbieter bearbeiten",
"Domain - Tooltip": "Benutzerdefinierte Domain für Objektspeicher",
"Edit Provider": "Provider bearbeiten",
"Email content": "Email-Inhalt",
"Email content - Tooltip": "Inhalt der E-Mail",
"Email sent successfully": "E-Mail erfolgreich gesendet",
"Email title": "Email-Titel",
"Email title - Tooltip": "Betreff der E-Mail",
"Enable QR code": "QR-Code aktivieren",
"Enable QR code - Tooltip": "Ob Scannen von QR-Code zum Einloggen erlaubt werden soll",
"Endpoint": "Endpunkt",
"Endpoint (Intranet)": "Endpunkt (Intranet)",
"Enable QR code - Tooltip": "Ob das Scannen von QR-Codes zum Einloggen aktiviert werden soll",
"Endpoint": "Endpoint",
"Endpoint (Intranet)": "Endpoint (Intranet)",
"Host": "Host",
"Host - Tooltip": "Name des Gastgebers",
"Host - Tooltip": "Name des Hosts",
"IdP": "IdP",
"IdP certificate": "IdP-Zertifikat",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Issuer URL": "Emittenten-URL",
"Issuer URL": "Issuer-URL",
"Issuer URL - Tooltip": "Emittenten-URL",
"Link copied to clipboard successfully": "Link wurde erfolgreich in die Zwischenablage kopiert",
"Metadata": "Metadaten",
"Metadata - Tooltip": "SAML-Metadaten",
"Method - Tooltip": "Anmeldeverfahren, QR-Code oder geräuschloser Login",
"New Provider": "Neuer Anbieter",
"Method - Tooltip": "Anmeldeverfahren, QR-Code oder Silent-Login",
"New Provider": "Neuer Provider",
"Normal": "Normal",
"Parse": "parsen",
"Parse metadata successfully": "Metadaten erfolgreich analysiert",
"Path prefix": "Pfadpräfix",
"Path prefix - Tooltip": "Bucket-Pfad-Präfix für Objektspeicher",
"Please use WeChat and scan the QR code to sign in": "Bitte verwende WeChat und scanne den QR-Code ein, um dich anzumelden",
"Port": "Hafen",
"Please use WeChat and scan the QR code to sign in": "Bitte verwenden Sie WeChat und scanne den QR-Code ein, um dich anzumelden",
"Port": "Port",
"Port - Tooltip": "Stellen Sie sicher, dass der Port offen ist",
"Prompted": "ausgelöst",
"Provider URL": "Anbieter-URL",
"Provider URL": "Provider-URL",
"Provider URL - Tooltip": "URL zur Konfiguration des Dienstanbieters, dieses Feld dient nur als Referenz und wird in Casdoor nicht verwendet",
"Region ID": "Regions-ID",
"Region ID - Tooltip": "Regionale ID für den Dienstleister",
"Region ID - Tooltip": "Regions-ID für den Dienstleister",
"Region endpoint for Internet": "Regionsendpunkt für das Internet",
"Region endpoint for Intranet": "Regionales Endpunkt für Intranet",
"Required": "Benötigt",
@@ -547,34 +547,34 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP Entity ID": "SP-Entitäts-ID",
"Scene": "Szene",
"Scene": "Scene",
"Scene - Tooltip": "Szene",
"Scope": "Umfang",
"Scope - Tooltip": "Umfang",
"Secret access key": "Geheimer Zugriffsschlüssel",
"Scope": "Scope",
"Scope - Tooltip": "Scope",
"Secret access key": "Secret-Access-Key",
"Secret access key - Tooltip": "Geheimer Zugriffsschlüssel",
"Secret key": "Geheimschlüssel",
"Secret key - Tooltip": "Vom Server verwendet, um die API des Verifizierungscodes-Anbieters für die Verifizierung aufzurufen",
"Secret key": "Secret-Key",
"Secret key - Tooltip": "Vom Server verwendet, um die API des Verifizierungscodes-Providers für die Verifizierung aufzurufen",
"Send Testing Email": "Senden Sie eine Test-E-Mail",
"Send Testing SMS": "Sende Test-SMS",
"Sign Name": "Unterschreiben Sie den Namen",
"Sign Name": "Signatur Namen",
"Sign Name - Tooltip": "Name der Signatur, die verwendet werden soll",
"Sign request": "Unterschriftsanforderung",
"Sign request - Tooltip": "Ob die Anfrage eine Unterschrift erfordert",
"Signin HTML": "Anmeldung HTML",
"Signin HTML - Edit": "Anmeldung HTML - Bearbeiten",
"Sign request": "Signaturanfrage",
"Sign request - Tooltip": "Ob die Anfrage eine Signatur erfordert",
"Signin HTML": "Anmeldungs-HTML",
"Signin HTML - Edit": "Anmeldungs-HTML - Bearbeiten",
"Signin HTML - Tooltip": "Benutzerdefiniertes HTML zur Ersetzung des Standard-Anmelde-Seitenstils",
"Signup HTML": "Anmeldung HTML",
"Signup HTML": "Registrierungs-HTML",
"Signup HTML - Edit": "Registrierung HTML - Bearbeiten",
"Signup HTML - Tooltip": "Benutzerdefiniertes HTML zur Ersetzung des Standard-Anmelde-Seitenstils",
"Signup HTML - Tooltip": "Benutzerdefiniertes HTML zur Ersetzung des Standard-Registrierungs-Seitenstils",
"Silent": "Silent",
"Site key": "Seitenschlüssel",
"Site key - Tooltip": "Site-Schlüssel",
"Site key": "Site-Key",
"Site key - Tooltip": "Seitenschlüssel",
"Sliding Validation": "Sliding Validation",
"Sub type": "Untertyp",
"Sub type - Tooltip": "Unterart",
"Template code": "Vorlagen-Code",
"Template code - Tooltip": "Vorlagen-Code",
"Template code": "Template-Code",
"Template code - Tooltip": "Template-Code",
"Test Email": "Test E-Mail",
"Test Email - Tooltip": "E-Mail-Adresse zum Empfangen von Test-E-Mails",
"Test SMTP Connection": "Testen Sie die SMTP-Verbindung",
@@ -585,23 +585,23 @@
"Type - Tooltip": "Wählen Sie einen Typ aus",
"UserInfo URL": "UserInfo-URL",
"UserInfo URL - Tooltip": "UserInfo-URL",
"admin (Shared)": "admin (Gemeinsam)"
"admin (Shared)": "admin (Shared)"
},
"record": {
"Is triggered": "Wird ausgelöst"
"Is triggered": "Ist ausgelöst"
},
"resource": {
"Copy Link": "Kopiere den Link",
"Copy Link": "Link kopieren",
"File name": "Dateiname",
"File size": "Dateigröße",
"Format": "Format",
"Parent": "Elternteil",
"Parent": "Parent",
"Upload a file...": "Hochladen einer Datei..."
},
"role": {
"Edit Role": "Rolle bearbeiten",
"New Role": "Neue Rolle",
"Sub domains": "Unterdomänen",
"Sub domains": "Subdomains",
"Sub domains - Tooltip": "In der aktuellen Rolle enthaltene Domains",
"Sub roles": "Unterrollen",
"Sub roles - Tooltip": "Rollen, die in der aktuellen Rolle enthalten sind",
@@ -612,11 +612,11 @@
"Accept": "Akzeptieren",
"Agreement": "Vereinbarung",
"Confirm": "Bestätigen",
"Decline": "Abnahme",
"Decline": "Ablehnen",
"Have account?": "Haben Sie ein Konto?",
"Please accept the agreement!": "Bitte akzeptieren Sie die Vereinbarung!",
"Please click the below button to sign in": "Bitte klicken Sie auf die untenstehende Schaltfläche, um sich anzumelden",
"Please confirm your password!": "Bitte bestätige dein Passwort!",
"Please click the below button to sign in": "Bitte klicken Sie auf den untenstehenden Button, um sich anzumelden",
"Please confirm your password!": "Bitte bestätigen Sie Ihr Passwort!",
"Please input the correct ID card number!": "Bitte geben Sie die korrekte Ausweisnummer ein!",
"Please input your Email!": "Bitte geben Sie Ihre E-Mail-Adresse ein!",
"Please input your ID card number!": "Bitte geben Sie Ihre Personalausweisnummer ein!",
@@ -653,17 +653,17 @@
"Database - Tooltip": "Der ursprüngliche Datenbankname",
"Database type": "Datenbanktyp",
"Database type - Tooltip": "Datenbanktyp, der alle Datenbanken unterstützt, die von XORM unterstützt werden, wie MySQL, PostgreSQL, SQL Server, Oracle, SQLite, usw.",
"Edit Syncer": "Edit Syncer",
"Edit Syncer": "Syncer bearbeiten",
"Error text": "Fehlermeldung",
"Error text - Tooltip": "Fehler Text",
"Is hashed": "wurde gehasht",
"Is hashed": "ist gehasht",
"New Syncer": "Neuer Syncer",
"Sync interval": "Synchronisierungsintervall",
"Sync interval - Tooltip": "Einheit in Sekunden",
"Table": "Tabelle",
"Table - Tooltip": "Name der Datenbanktabelle",
"Table columns": "Tabellenspalten",
"Table columns - Tooltip": "zu Deutsch: Spalten in der Tabelle, die an der Datensynchronisierung beteiligt sind. Spalten, die nicht an der Synchronisierung beteiligt sind, müssen nicht hinzugefügt werden",
"Table columns - Tooltip": "Spalten in der Tabelle, die an der Datensynchronisierung beteiligt sind. Spalten, die nicht an der Synchronisierung beteiligt sind, müssen nicht hinzugefügt werden",
"Table primary key": "Primärschlüssel der Tabelle",
"Table primary key - Tooltip": "Primärschlüssel in einer Tabelle, wie beispielsweise eine ID"
},
@@ -671,7 +671,7 @@
"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": "Gemeinschaft",
"Community": "Community",
"Failed to get CPU usage": "Konnte CPU-Auslastung nicht abrufen",
"Failed to get memory usage": "Fehler beim Abrufen der Speichernutzung",
"Memory Usage": "Speichernutzung",
@@ -680,29 +680,29 @@
"Version": "Version"
},
"theme": {
"Blossom": "Blüte",
"Border radius": "Eckradius",
"Compact": "Kompakt",
"Customize theme": "Anpassen des Themas",
"Dark": "Dunkelheit",
"Default": "Voreinstellung",
"Blossom": "Blossom",
"Border radius": "Border Radius",
"Compact": "Compact",
"Customize theme": "Anpassen des Themes",
"Dark": "Dunkel",
"Default": "Standardeinstellungen",
"Document": "Dokument",
"Is compact": "Ist kompakt",
"Primary color": "Primärfarbe",
"Theme": "Thema",
"Theme - Tooltip": "Stilthema der Anwendung"
"Theme": "Theme",
"Theme - Tooltip": "Stiltheme der Anwendung"
},
"token": {
"Access token": "Zugriffstoken",
"Access token": "Access-Token",
"Authorization code": "Authorisierungscode",
"Edit Token": "Edit-Token bearbeiten",
"Edit Token": "Token bearbeiten",
"Expires in": "läuft ab in",
"New Token": "Neues Token",
"New Token": "Neuer Token",
"Token type": "Token-Typ"
},
"user": {
"3rd-party logins": "3rd-Party-Logins",
"3rd-party logins - Tooltip": "Soziale Anmeldungen, die vom Benutzer verknüpft werden",
"3rd-party logins": "Drittanbieter-Logins",
"3rd-party logins - Tooltip": "Drittanbieter-Anmeldungen, die mit dem Benutzer verknüpft sind",
"Address": "Adresse",
"Address - Tooltip": "Wohnadresse",
"Affiliation": "Zugehörigkeit",
@@ -716,22 +716,22 @@
"Country/Region - Tooltip": "Land oder Region",
"Edit User": "Benutzer bearbeiten",
"Email cannot be empty": "E-Mail darf nicht leer sein",
"Email/phone reset successfully": "E-Mail-/Telefonrücksetzung erfolgreich durchgeführt",
"Empty input!": "Leereingabe!",
"Email/phone reset successfully": "E-Mail-/Telefon-Zurücksetzung erfolgreich durchgeführt",
"Empty input!": "Leere Eingabe!",
"Homepage": "Startseite des Benutzers",
"Homepage - Tooltip": "Homepage-URL des Benutzers",
"ID card": "Ausweis",
"Input your email": "Gib deine E-Mail-Adresse ein",
"Input your email": "Geben Sie Ihre E-Mail-Adresse ein",
"Input your phone number": "Geben Sie Ihre Telefonnummer ein",
"Is admin": "Ist Admin",
"Is admin - Tooltip": "Ist ein Administrator der Organisation, zu der der Benutzer gehört",
"Is deleted": "wurde gelöscht",
"Is deleted": "ist gelöscht",
"Is deleted - Tooltip": "\"Gelöschte Benutzer behalten nur Datenbankdatensätze und können keine Operationen ausführen.\"",
"Is forbidden": "Ist verboten",
"Is forbidden - Tooltip": "Verbotene Benutzer können sich nicht mehr einloggen",
"Is global admin": "Ist weltweiter Administrator",
"Is global admin": "Ist globaler Administrator",
"Is global admin - Tooltip": "Ist ein Administrator von Casdoor",
"Keys": "Schlüssel",
"Keys": "Keys",
"Link": "Link",
"Location": "Ort",
"Location - Tooltip": "Stadt des Wohnsitzes",
@@ -740,22 +740,22 @@
"New Email": "Neue E-Mail",
"New Password": "Neues Passwort",
"New User": "Neuer Benutzer",
"New phone": "Neues Telefon",
"New phone": "Neue Telefonnummer",
"Old Password": "Altes Passwort",
"Password set successfully": "Passwort erfolgreich festgelegt",
"Phone cannot be empty": "Telefon kann nicht leer sein",
"Phone cannot be empty": "Telefonnummer kann nicht leer sein",
"Please select avatar from resources": "Bitte wählen Sie einen Avatar aus den Ressourcen aus",
"Properties": "Eigenschaften",
"Properties - Tooltip": "Eigenschaften des Benutzers",
"Re-enter New": "Neueingabe betreten",
"Reset Email...": "Reset E-Mail...",
"Re-enter New": "Neueingabe wiederholen",
"Reset Email...": "E-Mail zurücksetzen...",
"Reset Phone...": "Telefon zurücksetzen...",
"Select a photo...": "Wählen Sie ein Foto aus...",
"Set Password": "Passwort festlegen",
"Set new profile picture": "Neues Profilbild festlegen",
"Set password...": "Passwort festlegen...",
"Tag": "Tag",
"Tag - Tooltip": "Tag des Benutzers",
"Tag - Tooltip": "Tags des Benutzers",
"Title": "Titel",
"Title - Tooltip": "Position in der Zugehörigkeit",
"Two passwords you typed do not match.": "Zwei von Ihnen eingegebene Passwörter stimmen nicht überein.",
@@ -765,10 +765,10 @@
"Values": "Werte",
"Verification code sent": "Bestätigungscode gesendet",
"WebAuthn credentials": "WebAuthn-Anmeldeinformationen",
"input password": "Eingabe des Passworts"
"input password": "Passwort eingeben"
},
"webhook": {
"Content type": "Inhaltstyp",
"Content type": "Content-Type",
"Content type - Tooltip": "Inhaltstyp",
"Edit Webhook": "Webhook bearbeiten",
"Events": "Ereignisse",
@@ -776,9 +776,9 @@
"Headers": "Überschriften",
"Headers - Tooltip": "HTTP-Header (Schlüssel-Wert-Paare)",
"Is user extended": "Wurde der Benutzer erweitert?",
"Is user extended - Tooltip": "Sollten die erweiterten Felder des Benutzers in das JSON eingeschlossen werden?",
"Is user extended - Tooltip": "Sollten die erweiterten Felder des Benutzers in das JSON inkludiert werden?",
"Method - Tooltip": "HTTP Methode",
"New Webhook": "Neuer Webhook",
"New Webhook": "Neue Webhook",
"Value": "Wert"
}
}