Compare commits

..

36 Commits

Author SHA1 Message Date
Leon
178cf7945d feat: improve token introspection endpoint (#534)
* feat: add introspection endpoint to oidc discovery endpoint

* fix: let introspect endpoint handle formData as spec define.

Signed-off-by: Leon <leondevlifelog@gmail.com>
2022-03-04 08:54:33 +08:00
Leon
ab5af979c8 feat: add Oauth 2.0 Token Introspection(rfc7662) endpoint support (#532)
Signed-off-by: Leon <leondevlifelog@gmail.com>
2022-03-03 17:48:47 +08:00
Gucheng Wang
e31aaf5657 Rename httpProxy. 2022-03-03 08:59:38 +08:00
Steve0x2a
eaf5cb66f3 fix: update authz rule list (#528)
* fix: update authz rule list

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: resolve conflicts.

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-03 00:52:28 +08:00
Yixiang Zhao
83a6b757a4 fix: password leakage vulnerability caused by pagination (#527)
* fix: password leakage vulnerability caused by pagination

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: unsafe get-app-login response fields

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-03-02 20:58:16 +08:00
Steve0x2a
2a0dcd746f feat: add token logout endpoint (#526)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-02 20:37:31 +08:00
Steve0x2a
22f5ad06ec fix: Make secret optional when using PKCE (#525)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-02 13:15:14 +08:00
Yang Luo
18aa70dfb2 Fix delete-resource authz failure. 2022-03-01 22:37:23 +08:00
Steve0x2a
697b3e4998 feat: add implicit flow support (#520)
* feat: add implicit flow support

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: idp support in implicit flow

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-01 19:09:59 +08:00
Steve0x2a
d48d515c36 fix: Missing extendedUser in signup webhook (#522)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-01 18:25:48 +08:00
Gucheng Wang
a5d166c35f Support language param. 2022-02-28 21:33:10 +08:00
Nekotoxin
4915963c52 fix: member No.0 bug (#516)
* fix: member No.0 bug

* Update account.go

* fix: member No.0 bug

* fix: member No.0 bug

* Update account.go
2022-02-28 19:42:11 +08:00
Nekotoxin
759a1421e5 feat: add the 'karma' prop to table User (#518)
* feature: feat : add the 'karma' prop to table User

* feat: add the 'karma' prop to table User
2022-02-28 16:25:09 +08:00
Gucheng Wang
c14bf9fdab Fix bug in first name, last name checking 2022-02-28 13:17:05 +08:00
Yang Luo
e19f07c521 Add product detail page. 2022-02-27 23:50:35 +08:00
Yang Luo
39ab71c5db Add product pages. 2022-02-27 20:09:19 +08:00
Steve0x2a
2c97f8a8b7 feat: add two authentication flow types (#512)
* feat: add two authentication flow types

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: delete implicit method

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: use a more appropriate name

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: apply suggestion

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: remove redundant code

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-02-27 14:05:07 +08:00
Yang Luo
21392dcc14 Support user's first name and last name. 2022-02-27 14:02:52 +08:00
Yang Luo
953d3d5bc5 Change personal to real name. 2022-02-27 13:44:44 +08:00
Yixiang Zhao
ddee97f544 fix: this.props.location undefined (#513)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-02-26 18:39:24 +08:00
Yang Luo
c58a6d8725 Set enableSigninSession to false by default. 2022-02-25 23:58:13 +08:00
Yang Luo
a5ff9549c1 Remove useless menu item. 2022-02-25 22:35:24 +08:00
Yang Luo
fe57dcbff4 Improve translation. 2022-02-25 21:31:15 +08:00
Yixiang Zhao
f8c4ca0f00 feat: add cancel buttons on the edit page (#509)
* feat: add cancel buttons on the edit page

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: remove warning and primary type of cancel buttons

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-02-25 18:16:02 +08:00
Jerry
e738c42bd8 fix: facebook login exceptions (#508)
* Fix the exception caused by "Username" being empty when logging in with facebook

* fix: facebook login missing "Username" exception
2022-02-23 23:58:17 +08:00
Steve0x2a
cbc8c58e85 fix: oidc jwks endpoint only return default cert (#506)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-02-21 23:17:16 +08:00
Gucheng Wang
07c90e048f Update personal name. 2022-02-21 16:01:39 +08:00
Steve0x2a
a33076ada4 feat: add AD-FS support (#505)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-02-20 15:01:48 +08:00
Ghost Lee
9cabc4035f fix: docker-compose.yml has duplicated label (#502)
the casdoor service config in docker-compose.yml has duplicated restart label
2022-02-20 14:15:57 +08:00
Steve0x2a
274096fe9d fix: empty iss return (#503)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-02-18 12:36:11 +08:00
Steve0x2a
661abd6b6e feat: add steam support (#497)
* feat: add steam support

Signed-off-by: 0x2a <stevesough@gmail.com>

* fix: wrong name

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-02-16 19:57:46 +08:00
Yixiang Zhao
4122c94205 feat: add pagination for LdapSyncPage and fix the bug Ldap auto-sync cannot disable (#496)
* feat: add pagination for LdapSyncPage

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: Ldap auto sync cannot disable

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-02-15 23:03:53 +08:00
Rafael Firmino
68ef5f8311 test: add tests in strings manipulation (#477)
* test: add tests in strings manipulation

Add tests 
improving functions like BoolToString, CamelToSnakeCase, GetMinLenStr and SnakeString

* Add copyrig

* test: fix tests description

* test: add tests for function manupulate string
2022-02-15 21:56:59 +08:00
大雄
e35b058ab4 feat: add helm manifest for k8s and makefile (#444)
Signed-off-by: henrywangx <henrywangx@gmail.com>

Co-authored-by: xiong wang <xiong.wang@inceptio.ai>
2022-02-15 21:47:13 +08:00
Gucheng Wang
7d1f368bc2 Support docx file upload. 2022-02-15 21:21:07 +08:00
Gucheng Wang
0bd86baf4d Fix crash in incremental ID. 2022-02-14 22:58:26 +08:00
97 changed files with 5984 additions and 344 deletions

3
.gitignore vendored
View File

@@ -13,7 +13,8 @@
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
vendor/
bin/
.idea/
*.iml

42
.golangci.yml Normal file
View File

@@ -0,0 +1,42 @@
linters:
disable-all: true
enable:
- deadcode
- dupl
- errcheck
- goconst
- gocyclo
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- prealloc
- staticcheck
- structcheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- revive
- exportloopref
run:
deadline: 5m
skip-dirs:
- api
# skip-files:
# - ".*_test\\.go$"
modules-download-mode: vendor
# all available settings of specific linters
linters-settings:
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 150
# tab width in spaces. Default to 1.
tab-width: 1

113
Makefile Normal file
View File

@@ -0,0 +1,113 @@
# Image URL to use all building/pushing image targets
REGISTRY ?= casbin
IMG ?= casdoor
IMG_TAG ?=$(shell git --no-pager log -1 --format="%ad" --date=format:"%Y%m%d")-$(shell git describe --tags --always --dirty --abbrev=6)
NAMESPACE ?= casdoor
APP ?= casdoor
HOST ?= test.com
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
# Setting SHELL to bash allows bash commands to be executed by recipes.
# This is a requirement for 'setup-envtest.sh' in the test target.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
.PHONY: all
all: docker-build docker-push deploy
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development
.PHONY: fmt
fmt: ## Run go fmt against code.
go fmt ./...
.PHONY: vet
vet: ## Run go vet against code.
go vet ./...
.PHONY: ut
ut: ## UT test
go test -v -cover -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
##@ Build
.PHONY: backend
backend: fmt vet ## Build backend binary.
go build -o bin/manager main.go
.PHONY: backend-vendor
backend-vendor: vendor fmt vet ## Build backend binary with vendor.
go build -mod=vendor -o bin/manager main.go
.PHONY: frontend
frontend: ## Build backend binary.
cd web/ && yarn && yarn run build && cd -
.PHONY: vendor
vendor: ## Update vendor.
go mod vendor
.PHONY: run
run: fmt vet ## Run backend in local
go run ./main.go
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
docker build -t ${REGISTRY}/${IMG}:${IMG_TAG} .
.PHONY: docker-push
docker-push: ## Push docker image with the manager.
docker push ${REGISTRY}/${IMG}:${IMG_TAG}
lint-install: ## Install golangci-lint
@# The following installs a specific version of golangci-lint, which is appropriate for a CI server to avoid different results from build to build
go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.40.1
lint: ## Run golangci-lint
@echo "---lint---"
golangci-lint run --modules-download-mode=vendor ./...
##@ Deployment
.PHONY: deploy
deploy: ## Deploy controller to the K8s cluster specified in ~/.kube/config.
helm upgrade --install ${APP} manifests/casdoor --create-namespace --set ingress.enabled=true \
--set "ingress.hosts[0].host=${HOST},ingress.hosts[0].paths[0].path=/,ingress.hosts[0].paths[0].pathType=ImplementationSpecific" \
--set image.tag=${IMG_TAG} --set image.repository=${REGISTRY} --set image.name=${IMG} --version ${IMG_TAG} -n ${NAMESPACE}
.PHONY: dry-run
dry-run: ## Dry run for helm install
helm upgrade --install ${APP} manifests/casdoor --set ingress.enabled=true \
--set "ingress.hosts[0].host=${HOST},ingress.hosts[0].paths[0].path=/,ingress.hosts[0].paths[0].pathType=ImplementationSpecific" \
--set image.tag=${IMG_TAG} --set image.repository=${REGISTRY} --set image.name=${IMG} --version ${IMG_TAG} -n ${NAMESPACE} --dry-run
.PHONY: undeploy
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
helm delete ${APP} -n ${NAMESPACE}

View File

@@ -158,6 +158,19 @@ dataSourceName = root:123456@tcp(db:3306)/
docker-compose up
```
### K8S
You could use helm to deploy casdoor in k8s. At first, you should modify the [configmap](./manifests/casdoor/templates/configmap.yaml) for your application.
And then run bellow command to deploy it.
```bash
IMG_TAG=latest make deploy
```
And undeploy it with:
```bash
make undeploy
```
That's it! Try to visit http://localhost:8000/. :small_airplane:
## Detailed documentation

View File

@@ -54,7 +54,7 @@ m = (r.subOwner == p.subOwner || p.subOwner == "*") && \
(r.urlPath == p.urlPath || p.urlPath == "*") && \
(r.objOwner == p.objOwner || p.objOwner == "*") && \
(r.objName == p.objName || p.objName == "*") || \
(r.urlPath == "/api/update-user" && r.subOwner == r.objOwner && r.subName == r.objName)
(r.subOwner == r.objOwner && r.subName == r.objName)
`
m, err := model.NewModelFromString(modelText)
@@ -83,14 +83,11 @@ p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, *
p, *, *, POST, /api/login/oauth/access_token, *, *
p, *, *, POST, /api/login/oauth/refresh_token, *, *
p, *, *, GET, /api/login/oauth/logout, *, *
p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-users, *, *
p, *, *, GET, /api/get-user, *, *
p, *, *, GET, /api/get-organizations, *, *
p, *, *, GET, /api/get-user-application, *, *
p, *, *, GET, /api/get-default-providers, *, *
p, *, *, GET, /api/get-resources, *, *
p, *, *, POST, /api/upload-avatar, *, *
p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, *
@@ -98,7 +95,7 @@ p, *, *, GET, /api/get-human-check, *, *
p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, *
p, *, *, *, /api/certs, *, *
p, *, *, *, /.well-known/jwks, *, *
p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *
`

View File

@@ -12,7 +12,7 @@ redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
httpProxy = "127.0.0.1:10808"
sock5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10
initScore = 2000
logPostOnly = true

View File

@@ -18,16 +18,16 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
const (
ResponseTypeLogin = "login"
ResponseTypeCode = "code"
ResponseTypeLogin = "login"
ResponseTypeCode = "code"
ResponseTypeToken = "token"
ResponseTypeIdToken = "id_token"
)
type RequestForm struct {
@@ -37,6 +37,8 @@ type RequestForm struct {
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
Phone string `json:"phone"`
Affiliation string `json:"affiliation"`
@@ -69,18 +71,6 @@ type Response struct {
Data2 interface{} `json:"data2"`
}
type Userinfo struct {
Sub string `json:"sub"`
Iss string `json:"iss"`
Aud string `json:"aud"`
Name string `json:"name,omitempty"`
DisplayName string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
Avatar string `json:"picture,omitempty"`
Address string `json:"address,omitempty"`
Phone string `json:"phone,omitempty"`
}
type HumanCheck struct {
Type string `json:"type"`
AppKey string `json:"appKey"`
@@ -116,7 +106,7 @@ func (c *ApiController) Signup() {
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", form.Organization))
msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.Email, form.Phone, form.Affiliation)
msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.FirstName, form.LastName, form.Email, form.Phone, form.Affiliation)
if msg != "" {
c.ResponseError(msg)
return
@@ -145,7 +135,12 @@ func (c *ApiController) Signup() {
id := util.GenerateId()
if application.GetSignupItemRule("ID") == "Incremental" {
lastUser := object.GetLastUser(form.Organization)
lastIdInt := util.ParseInt(lastUser.Id)
lastIdInt := -1
if lastUser != nil {
lastIdInt = util.ParseInt(lastUser.Id)
}
id = strconv.Itoa(lastIdInt + 1)
}
@@ -154,6 +149,8 @@ func (c *ApiController) Signup() {
username = id
}
userCount := object.GetUserCount(form.Organization, "", "") + 1
user := &object.User{
Owner: form.Organization,
Name: username,
@@ -176,6 +173,16 @@ func (c *ApiController) Signup() {
IsDeleted: false,
SignupApplication: application.Name,
Properties: map[string]string{},
Ranking: userCount + 1,
Karma: 0,
}
if application.GetSignupItemRule("Display name") == "First, last" {
if form.FirstName != "" || form.LastName != "" {
user.DisplayName = fmt.Sprintf("%s %s", form.FirstName, form.LastName)
user.FirstName = form.FirstName
user.LastName = form.LastName
}
}
affected := object.AddUser(user)
@@ -194,6 +201,11 @@ func (c *ApiController) Signup() {
object.DisableVerificationCode(form.Email)
object.DisableVerificationCode(checkPhone)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
go object.AddRecord(record)
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
c.ResponseOk(userId)
@@ -249,38 +261,18 @@ func (c *ApiController) GetAccount() {
// @Title UserInfo
// @Tag Account API
// @Description return user information according to OIDC standards
// @Success 200 {object} controllers.Userinfo The Response object
// @Success 200 {object} object.Userinfo The Response object
// @router /userinfo [get]
func (c *ApiController) GetUserinfo() {
userId, ok := c.RequireSignedIn()
if !ok {
return
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
return
}
scope, aud := c.GetSessionOidc()
iss := beego.AppConfig.String("origin")
resp := Userinfo{
Sub: user.Id,
Iss: iss,
Aud: aud,
}
if strings.Contains(scope, "profile") {
resp.Name = user.Name
resp.DisplayName = user.DisplayName
resp.Avatar = user.Avatar
}
if strings.Contains(scope, "email") {
resp.Email = user.Email
}
if strings.Contains(scope, "address") {
resp.Address = user.Location
}
if strings.Contains(scope, "phone") {
resp.Phone = user.Phone
host := c.Ctx.Request.Host
resp, err := object.GetUserInfo(userId, scope, aud, host)
if err != nil {
c.ResponseError(err.Error())
}
c.Data["json"] = resp
c.ServeJSON()

View File

@@ -38,6 +38,14 @@ func codeToResponse(code *object.Code) *Response {
return &Response{Status: "ok", Msg: "", Data: code.Code}
}
func tokenToResponse(token *object.Token) *Response {
if token.AccessToken == "" {
return &Response{Status: "error", Msg: "fail to get accessToken", Data: token.AccessToken}
}
return &Response{Status: "ok", Msg: "", Data: token.AccessToken}
}
// HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
userId := user.GetId()
@@ -59,13 +67,22 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
c.ResponseError("Challenge method should be S256")
return
}
code := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge)
code := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host)
resp = codeToResponse(code)
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else if form.Type == ResponseTypeToken || form.Type == ResponseTypeIdToken { //implicit flow
if !object.IsGrantTypeValid(form.Type, application.GrantTypes) {
resp = &Response{Status: "error", Msg: fmt.Sprintf("error: grant_type: %s is not supported in this application", form.Type), Data: ""}
} else {
scope := c.Input().Get("scope")
token, _ := object.GetTokenByUser(application, user, scope, c.Ctx.Request.Host)
resp = tokenToResponse(token)
}
} else {
resp = &Response{Status: "error", Msg: fmt.Sprintf("Unknown response type: %s", form.Type)}
}
@@ -101,6 +118,7 @@ func (c *ApiController) GetApplicationLogin() {
state := c.Input().Get("state")
msg, application := object.CheckOAuthLogin(clientId, responseType, redirectUri, scope, state)
application = object.GetMaskedApplication(application, "")
if msg != "" {
c.ResponseError(msg, application)
} else {
@@ -109,7 +127,7 @@ func (c *ApiController) GetApplicationLogin() {
}
func setHttpClient(idProvider idp.IdProvider, providerType string) {
if providerType == "GitHub" || providerType == "Google" || providerType == "Facebook" || providerType == "LinkedIn" {
if providerType == "GitHub" || providerType == "Google" || providerType == "Facebook" || providerType == "LinkedIn" || providerType == "Steam" {
idProvider.SetHttpClient(proxy.ProxyHttpClient)
} else {
idProvider.SetHttpClient(proxy.DefaultHttpClient)
@@ -221,7 +239,7 @@ func (c *ApiController) Login() {
clientSecret = provider.ClientSecret2
}
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri)
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain)
if idProvider == nil {
c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type))
return

View File

@@ -170,6 +170,7 @@ func (c *ApiController) UpdateLdap() {
return
}
prevLdap := object.GetLdap(ldap.Id)
affected := object.UpdateLdap(&ldap)
resp := wrapActionResponse(affected)
if affected {
@@ -177,6 +178,8 @@ func (c *ApiController) UpdateLdap() {
}
if ldap.AutoSync != 0 {
object.GetLdapAutoSynchronizer().StartAutoSync(ldap.Id)
} else if ldap.AutoSync == 0 && prevLdap.AutoSync != 0{
object.GetLdapAutoSynchronizer().StopAutoSync(ldap.Id)
}
c.Data["json"] = resp

View File

@@ -25,10 +25,10 @@ func (c *RootController) GetOidcDiscovery() {
c.ServeJSON()
}
// @Title GetOidcCert
// @Title GetJwks
// @Tag OIDC API
// @router /api/certs [get]
func (c *RootController) GetOidcCert() {
// @router /.well-known/jwks [get]
func (c *RootController) GetJwks() {
jwks, err := object.GetJsonWebKeySet()
if err != nil {
c.ResponseError(err.Error())

116
controllers/product.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 controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetProducts
// @Title GetProducts
// @Tag Product API
// @Description get products
// @Param owner query string true "The owner of products"
// @Success 200 {array} object.Product The Response object
// @router /get-products [get]
func (c *ApiController) GetProducts() {
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.GetProducts(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetProductCount(owner, field, value)))
products := object.GetPaginationProducts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(products, paginator.Nums())
}
}
// @Title GetProduct
// @Tag Product API
// @Description get product
// @Param id query string true "The id of the product"
// @Success 200 {object} object.Product The Response object
// @router /get-product [get]
func (c *ApiController) GetProduct() {
id := c.Input().Get("id")
c.Data["json"] = object.GetProduct(id)
c.ServeJSON()
}
// @Title UpdateProduct
// @Tag Product API
// @Description update product
// @Param id query string true "The id of the product"
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /update-product [post]
func (c *ApiController) UpdateProduct() {
id := c.Input().Get("id")
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateProduct(id, &product))
c.ServeJSON()
}
// @Title AddProduct
// @Tag Product API
// @Description add product
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /add-product [post]
func (c *ApiController) AddProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddProduct(&product))
c.ServeJSON()
}
// @Title DeleteProduct
// @Tag Product API
// @Description delete product
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-product [post]
func (c *ApiController) DeleteProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
c.ServeJSON()
}

View File

@@ -16,6 +16,7 @@ package controllers
import (
"encoding/json"
"net/http"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
@@ -149,8 +150,9 @@ func (c *ApiController) GetOAuthCode() {
c.ResponseError("Challenge method should be S256")
return
}
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge)
c.Data["json"] = object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, host)
c.ServeJSON()
}
@@ -170,17 +172,22 @@ func (c *ApiController) GetOAuthToken() {
clientSecret := c.Input().Get("client_secret")
code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier")
scope := c.Input().Get("scope")
username := c.Input().Get("username")
password := c.Input().Get("password")
if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier)
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host)
c.ServeJSON()
}
// RefreshToken
// @Title RefreshToken
// @Tag Token API
// @Description refresh OAuth access token
// @Param grant_type query string true "OAuth grant type"
// @Param refresh_token query string true "OAuth refresh token"
@@ -195,7 +202,92 @@ func (c *ApiController) RefreshToken() {
scope := c.Input().Get("scope")
clientId := c.Input().Get("client_id")
clientSecret := c.Input().Get("client_secret")
host := c.Ctx.Request.Host
c.Data["json"] = object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret)
c.Data["json"] = object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
c.ServeJSON()
}
// TokenLogout
// @Title TokenLogout
// @Tag Token API
// @Description delete token by AccessToken
// @Param id_token_hint query string true "id_token_hint"
// @Param post_logout_redirect_uri query string false "post_logout_redirect_uri"
// @Param state query string true "state"
// @Success 200 {object} controllers.Response The Response object
// @router /login/oauth/logout [get]
func (c *ApiController) TokenLogout() {
token := c.Input().Get("id_token_hint")
flag, application := object.DeleteTokenByAceessToken(token)
redirectUri := c.Input().Get("post_logout_redirect_uri")
state := c.Input().Get("state")
if application != nil && object.CheckRedirectUriValid(application, redirectUri) {
c.Ctx.Redirect(http.StatusFound, redirectUri+"?state="+state)
return
}
c.Data["json"] = wrapActionResponse(flag)
c.ServeJSON()
}
// IntrospectToken
// @Title IntrospectToken
// @Description The introspection endpoint is an OAuth 2.0 endpoint that takes a
// parameter representing an OAuth 2.0 token and returns a JSON document
// representing the meta information surrounding the
// token, including whether this token is currently active.
// This endpoint only support Basic Authorization.
// @Param token formData string true "access_token's value or refresh_token's value"
// @Param token_type_hint formData string true "the token type access_token or refresh_token"
// @Success 200 {object} object.IntrospectionResponse The Response object
// @router /login/oauth/introspect [post]
func (c *ApiController) IntrospectToken() {
tokenValue := c.Input().Get("token")
clientId, clientSecret, ok := c.Ctx.Request.BasicAuth()
if !ok {
util.LogWarning(c.Ctx, "Basic Authorization parses failed")
c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"}
c.ServeJSON()
return
}
application := object.GetApplicationByClientId(clientId)
if application == nil || application.ClientSecret != clientSecret {
util.LogWarning(c.Ctx, "Basic Authorization failed")
c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"}
c.ServeJSON()
return
}
token := object.GetTokenByTokenAndApplication(tokenValue, application.Name)
if token == nil {
util.LogWarning(c.Ctx, "application: %s can not find token", application.Name)
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil {
// and token revoked case. but we not implement
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
// refs: https://tools.ietf.org/html/rfc7009
util.LogWarning(c.Ctx, "token invalid")
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
c.Data["json"] = &object.IntrospectionResponse{
Active: true,
Scope: jwtToken.Scope,
ClientId: clientId,
Username: token.User,
TokenType: token.TokenType,
Exp: jwtToken.ExpiresAt.Unix(),
Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(),
Sub: jwtToken.Subject,
Aud: jwtToken.Audience,
Iss: jwtToken.Issuer,
Jti: jwtToken.Id,
}
c.ServeJSON()
}

View File

@@ -44,6 +44,7 @@ func (c *ApiController) GetGlobalUsers() {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetGlobalUserCount(field, value)))
users := object.GetPaginationGlobalUsers(paginator.Offset(), limit, field, value, sortField, sortOrder)
users = object.GetMaskedUsers(users)
c.ResponseOk(users, paginator.Nums())
}
}
@@ -70,6 +71,7 @@ func (c *ApiController) GetUsers() {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetUserCount(owner, field, value)))
users := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
users = object.GetMaskedUsers(users)
c.ResponseOk(users, paginator.Nums())
}
}

3
go.mod
View File

@@ -17,6 +17,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.1.0
github.com/google/uuid v1.2.0
github.com/jinzhu/configor v1.2.1 // indirect
github.com/lestrrat-go/jwx v0.9.0
github.com/markbates/goth v1.68.1-0.20211006204042-9dc8905b41c8
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76
@@ -25,7 +26,7 @@ require (
github.com/russellhaering/goxmldsig v1.1.1
github.com/satori/go.uuid v1.2.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.6.1
github.com/stretchr/testify v1.7.0
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3

3
go.sum
View File

@@ -343,8 +343,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=

136
idp/adfs.go Normal file
View File

@@ -0,0 +1,136 @@
// 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 idp
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"golang.org/x/oauth2"
)
type AdfsIdProvider struct {
Client *http.Client
Config *oauth2.Config
Host string
}
func NewAdfsIdProvider(clientId string, clientSecret string, redirectUrl string, hostUrl string) *AdfsIdProvider {
idp := &AdfsIdProvider{}
config := idp.getConfig(hostUrl)
config.ClientID = clientId
config.ClientSecret = clientSecret
config.RedirectURL = redirectUrl
idp.Config = config
idp.Host = hostUrl
return idp
}
func (idp *AdfsIdProvider) SetHttpClient(client *http.Client) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
idp.Client = client
idp.Client.Transport = tr
}
func (idp *AdfsIdProvider) getConfig(hostUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/adfs/oauth2/authorize", hostUrl),
TokenURL: fmt.Sprintf("%s/adfs/oauth2/token", hostUrl),
}
var config = &oauth2.Config{
Endpoint: endpoint,
}
return config
}
type AdfsToken struct {
IdToken string `json:"id_token"`
ExpiresIn int `json:"expires_in"`
ErrMsg string `json:"error_description"`
}
// get more detail via: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#request-an-access-token
func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
payload := url.Values{}
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_id", idp.Config.ClientID)
payload.Set("redirect_uri", idp.Config.RedirectURL)
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
pToken := &AdfsToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
}
if pToken.ErrMsg != "" {
return nil, fmt.Errorf("pToken.Errmsg = %s", pToken.ErrMsg)
}
token := &oauth2.Token{
AccessToken: pToken.IdToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
}
return token, nil
}
// Since the userinfo endpoint of ADFS only returns sub,
// the id_token is used to resolve the userinfo
func (idp *AdfsIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
resp, err := idp.Client.Get(fmt.Sprintf("%s/adfs/discovery/keys", idp.Host))
if err != nil {
return nil, err
}
keyset, err := jwk.Parse(resp.Body)
if err != nil {
return nil, err
}
tokenSrc := []byte(token.AccessToken)
publicKey, _ := keyset.Keys[0].Materialize()
id_token, _ := jwt.Parse(bytes.NewReader(tokenSrc), jwt.WithVerify(jwa.RS256, publicKey))
sid, _ := id_token.Get("sid")
upn, _ := id_token.Get("upn")
name, _ := id_token.Get("unique_name")
userinfo := &UserInfo{
Id: sid.(string),
Username: name.(string),
DisplayName: name.(string),
Email: upn.(string),
}
return userinfo, nil
}

View File

@@ -122,9 +122,9 @@ func (idp *FacebookIdProvider) GetToken(code string) (*oauth2.Token, error) {
//}
type FacebookUserInfo struct {
Id string `json:"id"` // The app user's App-Scoped User ID. This ID is unique to the app and cannot be used by other apps.
Name string `json:"name"` // The person's full name.
NameFormat string `json:"name_format"` // The person's name formatted to correctly handle Chinese, Japanese, or Korean ordering.
Id string `json:"id"` // The app user's App-Scoped User ID. This ID is unique to the app and cannot be used by other apps.
Name string `json:"name"` // The person's full name.
NameFormat string `json:"name_format"` // The person's name formatted to correctly handle Chinese, Japanese, or Korean ordering.
Picture struct { // The person's profile picture.
Data struct { // This struct is different as https://developers.facebook.com/docs/graph-api/reference/user/picture/
Height int `json:"height"`
@@ -164,6 +164,7 @@ func (idp *FacebookIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
userInfo := UserInfo{
Id: facebookUserInfo.Id,
Username: facebookUserInfo.Name,
DisplayName: facebookUserInfo.Name,
Email: facebookUserInfo.Email,
AvatarUrl: facebookUserInfo.Picture.Data.Url,

View File

@@ -45,6 +45,7 @@ import (
"github.com/markbates/goth/providers/salesforce"
"github.com/markbates/goth/providers/shopify"
"github.com/markbates/goth/providers/slack"
"github.com/markbates/goth/providers/steam"
"github.com/markbates/goth/providers/tumblr"
"github.com/markbates/goth/providers/twitter"
"github.com/markbates/goth/providers/yahoo"
@@ -171,6 +172,11 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
Provider: slack.New(clientId, clientSecret, redirectUrl),
Session: &slack.Session{},
}
case "Steam":
idp = GothIdProvider{
Provider: steam.New(clientSecret, redirectUrl),
Session: &steam.Session{},
}
case "Tumblr":
idp = GothIdProvider{
Provider: tumblr.New(clientId, clientSecret, redirectUrl),
@@ -209,10 +215,21 @@ func (idp *GothIdProvider) SetHttpClient(client *http.Client) {
func (idp *GothIdProvider) GetToken(code string) (*oauth2.Token, error) {
var expireAt time.Time
//Need to construct variables supported by goth
//to call the function to obtain accessToken
value := url.Values{}
value.Add("code", code)
var value url.Values
var err error
if idp.Provider.Name() == "steam" {
value, err = url.ParseQuery(code)
returnUrl := reflect.ValueOf(idp.Session).Elem().FieldByName("CallbackURL")
returnUrl.Set(reflect.ValueOf(value.Get("openid.return_to")))
if err != nil {
return nil, err
}
} else {
//Need to construct variables supported by goth
//to call the function to obtain accessToken
value = url.Values{}
value.Add("code", code)
}
accessToken, err := idp.Session.Authorize(idp.Provider, value)
//Get ExpiresAt's value
valueOfExpire := reflect.ValueOf(idp.Session).Elem().FieldByName("ExpiresAt")
@@ -231,10 +248,10 @@ func (idp *GothIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
if err != nil {
return nil, err
}
return getUser(gothUser), nil
return getUser(gothUser, idp.Provider.Name()), nil
}
func getUser(gothUser goth.User) *UserInfo {
func getUser(gothUser goth.User, provider string) *UserInfo {
user := UserInfo{
Id: gothUser.UserID,
Username: gothUser.Name,
@@ -258,7 +275,10 @@ func getUser(gothUser goth.User) *UserInfo {
user.DisplayName = user.Username
}
}
if provider == "steam" {
user.Username = user.DisplayName
user.Email = ""
}
return &user
}

View File

@@ -35,7 +35,7 @@ type IdProvider interface {
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string) IdProvider {
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string) IdProvider {
if typ == "GitHub" {
return NewGithubIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Google" {
@@ -66,6 +66,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return NewLarkIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "GitLab" {
return NewGitlabIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Adfs" {
return NewAdfsIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Baidu" {
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Infoflow" {
@@ -83,7 +85,7 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return nil
}
var gothList = []string{"Apple", "AzureAd", "Slack"}
var gothList = []string{"Apple", "AzureAd", "Slack", "Steam"}
func isGothSupport(provider string) bool {
for _, value := range gothList {

View File

@@ -16,6 +16,7 @@ package main
import (
"flag"
"fmt"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
@@ -65,8 +66,8 @@ func main() {
if err != nil {
panic(err)
}
port := beego.AppConfig.DefaultInt("httpport", 8000)
//logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false)
beego.Run()
beego.Run(fmt.Sprintf(":%v", port))
}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: casdoor
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

View File

@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "casdoor.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "casdoor.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "casdoor.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "casdoor.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "casdoor.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "casdoor.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "casdoor.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "casdoor.labels" -}}
helm.sh/chart: {{ include "casdoor.chart" . }}
{{ include "casdoor.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "casdoor.selectorLabels" -}}
app.kubernetes.io/name: {{ include "casdoor.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "casdoor.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "casdoor.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,23 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: casdoor-config
data:
app.conf: |
appname = casdoor
httpport = 80
runmode = dev
SessionOn = true
copyrequestbody = true
driverName = mysql
dataSourceName = root:123456@tcp(localhost:3306)/
dbName = casdoor
redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
sock5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10
initScore = 2000
logPostOnly = true
origin = "https://door.casbin.com"

View File

@@ -0,0 +1,75 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "casdoor.fullname" . }}
labels:
{{- include "casdoor.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "casdoor.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "casdoor.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "casdoor.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
# command: ["sleep", "100000000"]
ports:
- name: http
containerPort: 80
protocol: TCP
# livenessProbe:
# httpGet:
# path: /
# port: http
# readinessProbe:
# httpGet:
# path: /
# port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config-volume
mountPath: /conf
volumes:
- name: config-volume
projected:
defaultMode: 420
sources:
- configMap:
items:
- key: app.conf
path: app.conf
name: casdoor-config
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,28 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "casdoor.fullname" . }}
labels:
{{- include "casdoor.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "casdoor.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "casdoor.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "casdoor.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "casdoor.fullname" . }}
labels:
{{- include "casdoor.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "casdoor.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "casdoor.serviceAccountName" . }}
labels:
{{- include "casdoor.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "casdoor.fullname" . }}-test-connection"
labels:
{{- include "casdoor.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "casdoor.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,83 @@
# Default values for casdoor.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: casbin
name: casdoor-all-in-one
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -17,7 +17,6 @@ package object
import (
"fmt"
"runtime"
"xorm.io/core"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/conf"
@@ -25,6 +24,7 @@ import (
//_ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql
//_ "github.com/lib/pq" // db = postgres
"xorm.io/core"
"xorm.io/xorm"
)
@@ -183,6 +183,11 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Product))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Payment))
if err != nil {
panic(err)

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
@@ -38,6 +39,7 @@ type Application struct {
EnableCodeSignin bool `json:"enableCodeSignin"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
@@ -215,7 +217,19 @@ func GetMaskedApplication(application *Application, userId string) *Application
if application.ClientSecret != "" {
application.ClientSecret = "***"
}
return application
if application.OrganizationObj != nil {
if application.OrganizationObj.MasterPassword != "" {
application.OrganizationObj.MasterPassword = "***"
}
if application.OrganizationObj.PasswordType != "" {
application.OrganizationObj.PasswordType = "***"
}
if application.OrganizationObj.PasswordSalt != "" {
application.OrganizationObj.PasswordSalt = "***"
}
}
return application
}
func GetMaskedApplications(applications []*Application, userId string) []*Application {
@@ -282,3 +296,15 @@ func DeleteApplication(application *Application) bool {
func (application *Application) GetId() string {
return fmt.Sprintf("%s/%s", application.Owner, application.Name)
}
func CheckRedirectUriValid(application *Application, redirectUri string) bool {
var validUri = false
for _, tmpUri := range application.RedirectUris {
fmt.Println(tmpUri, redirectUri)
if strings.Contains(redirectUri, tmpUri) {
validUri = true
break
}
}
return validUri
}

View File

@@ -33,7 +33,7 @@ func init() {
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, email string, phone string, affiliation string) string {
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, firstName string, lastName string, email string, phone string, affiliation string) string {
if organization == nil {
return "organization does not exist"
}
@@ -85,11 +85,19 @@ func CheckUserSignup(application *Application, organization *Organization, usern
}
if application.IsSignupItemVisible("Display name") {
if displayName == "" {
return "displayName cannot be blank"
} else if application.GetSignupItemRule("Display name") == "Personal" {
if !isValidPersonalName(displayName) {
return "displayName is not valid personal name"
if application.GetSignupItemRule("Display name") == "First, last" && (firstName != "" || lastName != "") {
if firstName == "" {
return "firstName cannot be blank"
} else if lastName == "" {
return "lastName cannot be blank"
}
} else {
if displayName == "" {
return "displayName cannot be blank"
} else if application.GetSignupItemRule("Display name") == "Real name" {
if !isValidRealName(displayName) {
return "displayName is not valid real name"
}
}
}
}

View File

@@ -16,16 +16,16 @@ package object
import "regexp"
var rePersonalName *regexp.Regexp
var reRealName *regexp.Regexp
func init() {
var err error
rePersonalName, err = regexp.Compile("^[\u4E00-\u9FA5]{2,3}(?:·[\u4E00-\u9FA5]{2,3})*$")
reRealName, err = regexp.Compile("^[\u4E00-\u9FA5]{2,3}(?:·[\u4E00-\u9FA5]{2,3})*$")
if err != nil {
panic(err)
}
}
func isValidPersonalName(s string) bool {
return rePersonalName.MatchString(s)
func isValidRealName(s string) bool {
return reRealName.MatchString(s)
}

View File

@@ -162,7 +162,7 @@ func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) {
searchReq := goldap.NewSearchRequest(baseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
SearchFilter, SearchAttributes, nil)
searchResult, err := l.Conn.Search(searchReq)
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
if err != nil {
return nil, err
}

View File

@@ -65,6 +65,13 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) {
ticker := time.NewTicker(time.Duration(ldap.AutoSync) * time.Minute)
defer ticker.Stop()
for {
select {
case <-stopChan:
logs.Info(fmt.Sprintf("autoSync goroutine for %s stopped", ldap.Id))
return
case <-ticker.C:
}
UpdateLdapSyncTime(ldap.Id)
//fetch all users
conn, err := GetLdapConn(ldap.Host, ldap.Port, ldap.Admin, ldap.Passwd)
@@ -84,12 +91,6 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) {
} else {
logs.Info(fmt.Sprintf("ldap autosync success, %d new users, %d existing users", len(users)-len(*existed), len(*existed)))
}
select {
case <-stopChan:
logs.Info(fmt.Sprintf("autoSync goroutine for %s stopped", ldap.Id))
return
case <-ticker.C:
}
}
}

View File

@@ -30,6 +30,7 @@ type OidcDiscovery struct {
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
JwksUri string `json:"jwks_uri"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
ResponseTypesSupported []string `json:"response_types_supported"`
ResponseModesSupported []string `json:"response_modes_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
@@ -69,11 +70,12 @@ func GetOidcDiscovery(host string) OidcDiscovery {
// https://accounts.google.com/.well-known/openid-configuration
// https://access.line.me/.well-known/openid-configuration
oidcDiscovery := OidcDiscovery{
Issuer: originFrontend,
Issuer: originBackend,
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
JwksUri: fmt.Sprintf("%s/api/certs", originBackend),
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
ResponseTypesSupported: []string{"id_token"},
ResponseModesSupported: []string{"login", "code", "link"},
GrantTypesSupported: []string{"password", "authorization_code"},
@@ -89,21 +91,22 @@ func GetOidcDiscovery(host string) OidcDiscovery {
}
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
cert := GetDefaultCert()
certs := GetCerts("admin")
jwks := jose.JSONWebKeySet{}
//follows the protocol rfc 7517(draft)
//link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
//or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
certPemBlock := []byte(cert.PublicKey)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
for _, cert := range certs {
certPemBlock := []byte(cert.PublicKey)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey
jwk.Certificates = []*x509.Certificate{x509Cert}
jwk.KeyID = cert.Name
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey
jwk.Certificates = []*x509.Certificate{x509Cert}
jwk.KeyID = cert.Name
jwks.Keys = append(jwks.Keys, jwk)
}
var jwks jose.JSONWebKeySet
jwks.Keys = []jose.JSONWebKey{jwk}
return jwks, nil
}

130
object/product.go Normal file
View File

@@ -0,0 +1,130 @@
// 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"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Product 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"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Image string `xorm:"varchar(100)" json:"image"`
Detail string `xorm:"varchar(100)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price int `json:"price"`
Quantity int `json:"quantity"`
Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"`
State string `xorm:"varchar(100)" json:"state"`
}
func GetProductCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Product{})
if err != nil {
panic(err)
}
return int(count)
}
func GetProducts(owner string) []*Product {
products := []*Product{}
err := adapter.Engine.Desc("created_time").Find(&products, &Product{Owner: owner})
if err != nil {
panic(err)
}
return products
}
func GetPaginationProducts(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Product {
products := []*Product{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&products)
if err != nil {
panic(err)
}
return products
}
func getProduct(owner string, name string) *Product {
if owner == "" || name == "" {
return nil
}
product := Product{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&product)
if err != nil {
panic(err)
}
if existed {
return &product
} else {
return nil
}
}
func GetProduct(id string) *Product {
owner, name := util.GetOwnerAndNameFromId(id)
return getProduct(owner, name)
}
func UpdateProduct(id string, product *Product) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getProduct(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(product)
if err != nil {
panic(err)
}
return affected != 0
}
func AddProduct(product *Product) bool {
affected, err := adapter.Engine.Insert(product)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteProduct(product *Product) bool {
affected, err := adapter.Engine.ID(core.PK{product.Owner, product.Name}).Delete(&Product{})
if err != nil {
panic(err)
}
return affected != 0
}
func (product *Product) GetId() string {
return fmt.Sprintf("%s/%s", product.Owner, product.Name)
}

View File

@@ -17,6 +17,7 @@ package object
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
@@ -59,6 +60,21 @@ type TokenWrapper struct {
Scope string `json:"scope"`
}
type IntrospectionResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
ClientId string `json:"client_id,omitempty"`
Username string `json:"username,omitempty"`
TokenType string `json:"token_type,omitempty"`
Exp int64 `json:"exp,omitempty"`
Iat int64 `json:"iat,omitempty"`
Nbf int64 `json:"nbf,omitempty"`
Sub string `json:"sub,omitempty"`
Aud []string `json:"aud,omitempty"`
Iss string `json:"iss,omitempty"`
Jti string `json:"jti,omitempty"`
}
func GetTokenCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Token{})
@@ -168,6 +184,25 @@ func DeleteToken(token *Token) bool {
return affected != 0
}
func DeleteTokenByAceessToken(accessToken string) (bool, *Application) {
token := Token{AccessToken: accessToken}
existed, err := adapter.Engine.Get(&token)
if err != nil {
panic(err)
}
if !existed {
return false, nil
}
application := getApplication(token.Owner, token.Application)
affected, err := adapter.Engine.Where("access_token=?", accessToken).Delete(&Token{})
if err != nil {
panic(err)
}
return affected != 0, application
}
func GetTokenByAccessToken(accessToken string) *Token {
//Check if the accessToken is in the database
token := Token{AccessToken: accessToken}
@@ -178,9 +213,18 @@ func GetTokenByAccessToken(accessToken string) *Token {
return &token
}
func GetTokenByTokenAndApplication(token string, application string) *Token {
tokenResult := Token{}
existed, err := adapter.Engine.Where("(refresh_token = ? or access_token = ? ) and application = ?", token, token, application).Get(&tokenResult)
if err != nil || !existed {
return nil
}
return &tokenResult
}
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string) (string, *Application) {
if responseType != "code" {
return "response_type should be \"code\"", nil
if responseType != "code" && responseType != "token" && responseType != "id_token" {
return fmt.Sprintf("error: grant_type: %s is not supported in this application", responseType), nil
}
application := GetApplicationByClientId(clientId)
@@ -204,7 +248,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return "", application
}
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string) *Code {
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string) *Code {
user := GetUser(userId)
if user == nil {
return &Code{
@@ -227,7 +271,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
accessToken, refreshToken, err := generateJwtToken(application, user, nonce, scope)
accessToken, refreshToken, err := generateJwtToken(application, user, nonce, scope, host)
if err != nil {
panic(err)
}
@@ -261,7 +305,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string) *TokenWrapper {
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string) *TokenWrapper {
application := GetApplicationByClientId(clientId)
if application == nil {
return &TokenWrapper{
@@ -272,75 +316,30 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
}
}
if grantType != "authorization_code" {
//Check if grantType is allowed in the current application
if !IsGrantTypeValid(grantType, application.GrantTypes) {
return &TokenWrapper{
AccessToken: "error: grant_type should be \"authorization_code\"",
AccessToken: fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType),
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if code == "" {
return &TokenWrapper{
AccessToken: "error: authorization code should not be empty",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
var token *Token
var err error
switch grantType {
case "authorization_code": // Authorization Code Grant
token, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
case "password": // Resource Owner Password Credentials Grant
token, err = GetPasswordToken(application, username, password, scope, host)
case "client_credentials": // Client Credentials Grant
token, err = GetClientCredentialsToken(application, clientSecret, scope, host)
}
token := getTokenByCode(code)
if token == nil {
if err != nil {
return &TokenWrapper{
AccessToken: "error: invalid authorization code",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if application.Name != token.Application {
return &TokenWrapper{
AccessToken: "error: the token is for wrong application (client_id)",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if application.ClientSecret != clientSecret {
return &TokenWrapper{
AccessToken: "error: invalid client_secret",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
return &TokenWrapper{
AccessToken: "error: incorrect code_verifier",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if token.CodeIsUsed {
// anti replay attacks
return &TokenWrapper{
AccessToken: "error: authorization code has been used",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if time.Now().Unix() > token.CodeExpireIn {
// code must be used within 5 minutes
return &TokenWrapper{
AccessToken: "error: authorization code has expired",
AccessToken: err.Error(),
TokenType: "",
ExpiresIn: 0,
Scope: "",
@@ -361,7 +360,7 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
return tokenWrapper
}
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string) *TokenWrapper {
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) *TokenWrapper {
// check parameters
if grantType != "refresh_token" {
return &TokenWrapper{
@@ -420,7 +419,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
Scope: "",
}
}
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope)
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
panic(err)
}
@@ -459,3 +458,149 @@ func pkceChallenge(verifier string) string {
challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:])
return challenge
}
// Check if grantType is allowed in the current application
// authorization_code is allowed by default
func IsGrantTypeValid(method string, grantTypes []string) bool {
if method == "authorization_code" {
return true
}
for _, m := range grantTypes {
if m == method {
return true
}
}
return false
}
// Authorization code flow
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, error) {
if code == "" {
return nil, errors.New("error: authorization code should not be empty")
}
token := getTokenByCode(code)
if token == nil {
return nil, errors.New("error: invalid authorization code")
}
if token.CodeIsUsed {
// anti replay attacks
return nil, errors.New("error: authorization code has been used")
}
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
return nil, errors.New("error: incorrect code_verifier")
}
if application.ClientSecret != clientSecret {
// when using PKCE, the Client Secret can be empty,
// but if it is provided, it must be accurate.
if token.CodeChallenge == "" {
return nil, errors.New("error: invalid client_secret")
} else {
if clientSecret != "" {
return nil, errors.New("error: invalid client_secret")
}
}
}
if application.Name != token.Application {
return nil, errors.New("error: the token is for wrong application (client_id)")
}
if time.Now().Unix() > token.CodeExpireIn {
// code must be used within 5 minutes
return nil, errors.New("error: authorization code has expired")
}
return token, nil
}
// Resource Owner Password Credentials flow
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, error) {
user := getUser(application.Organization, username)
if user == nil {
return nil, errors.New("error: the user does not exist")
}
if user.Password != password {
return nil, errors.New("error: invalid username or password")
}
if user.IsForbidden {
return nil, errors.New("error: the user is forbidden to sign in, please contact the administrator")
}
accessToken, refreshToken, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, err
}
token := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Application: application.Name,
Organization: user.Owner,
User: user.Name,
Code: util.GenerateClientId(),
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
CodeIsUsed: true,
}
AddToken(token)
return token, nil
}
// Client Credentials flow
func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, error) {
if application.ClientSecret != clientSecret {
return nil, errors.New("error: invalid client_secret")
}
nullUser := &User{
Name: fmt.Sprintf("app/%s", application.Name),
}
accessToken, _, err := generateJwtToken(application, nullUser, "", scope, host)
if err != nil {
return nil, err
}
token := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Application: application.Name,
Organization: application.Organization,
User: nullUser.Name,
Code: util.GenerateClientId(),
AccessToken: accessToken,
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
CodeIsUsed: true,
}
AddToken(token)
return token, nil
}
// Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, host string) (*Token, error) {
accessToken, refreshToken, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, err
}
token := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Application: application.Name,
Organization: user.Owner,
User: user.Name,
Code: util.GenerateClientId(),
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
CodeIsUsed: true,
}
AddToken(token)
return token, nil
}

View File

@@ -61,12 +61,17 @@ func getShortClaims(claims Claims) ClaimsShort {
return res
}
func generateJwtToken(application *Application, user *User, nonce string, scope string) (string, string, error) {
func generateJwtToken(application *Application, user *User, nonce string, scope string, host string) (string, string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
user.Password = ""
origin := beego.AppConfig.String("origin")
_, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
claims := Claims{
User: user,
@@ -75,7 +80,7 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
Tag: user.Tag,
Scope: scope,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: beego.AppConfig.String("origin"),
Issuer: originBackend,
Subject: user.Id,
Audience: []string{application.ClientId},
ExpiresAt: jwt.NewNumericDate(expireTime),
@@ -142,3 +147,7 @@ func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
return nil, err
}
func ParseJwtTokenByApplication(token string, application *Application) (*Claims, error) {
return ParseJwtToken(token, getCertByApplication(application))
}

View File

@@ -18,6 +18,7 @@ import (
"fmt"
"strings"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@@ -33,6 +34,8 @@ type User struct {
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
@@ -52,6 +55,7 @@ type User struct {
Birthday string `xorm:"varchar(100)" json:"birthday"`
Education string `xorm:"varchar(100)" json:"education"`
Score int `json:"score"`
Karma int `json:"karma"`
Ranking int `json:"ranking"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
IsOnline bool `json:"isOnline"`
@@ -79,16 +83,30 @@ type User struct {
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
Lark string `xorm:"lark varchar(100)" json:"lark"`
Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"`
Adfs string `xorm:"adfs varchar(100)" json:"adfs"`
Baidu string `xorm:"baidu varchar(100)" json:"baidu"`
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
Apple string `xorm:"apple varchar(100)" json:"apple"`
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
Slack string `xorm:"slack varchar(100)" json:"slack"`
Steam string `xorm:"steam varchar(100)" json:"steam"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
}
type Userinfo struct {
Sub string `json:"sub"`
Iss string `json:"iss"`
Aud string `json:"aud"`
Name string `json:"name,omitempty"`
DisplayName string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
Avatar string `json:"picture,omitempty"`
Address string `json:"address,omitempty"`
Phone string `json:"phone,omitempty"`
}
func GetGlobalUserCount(field, value string) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&User{})
@@ -401,6 +419,39 @@ func DeleteUser(user *User) bool {
return affected != 0
}
func GetUserInfo(userId string, scope string, aud string, host string) (*Userinfo, error) {
user := GetUser(userId)
if user == nil {
return nil, fmt.Errorf("the user: %s doesn't exist", userId)
}
origin := beego.AppConfig.String("origin")
_, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
resp := Userinfo{
Sub: user.Id,
Iss: originBackend,
Aud: aud,
}
if strings.Contains(scope, "profile") {
resp.Name = user.Name
resp.DisplayName = user.DisplayName
resp.Avatar = user.Avatar
}
if strings.Contains(scope, "email") {
resp.Email = user.Email
}
if strings.Contains(scope, "address") {
resp.Address = user.Location
}
if strings.Contains(scope, "phone") {
resp.Phone = user.Phone
}
return &resp, nil
}
func LinkUserAccount(user *User, field string, value string) bool {
return SetUserField(user, field, value)
}

View File

@@ -54,17 +54,17 @@ func isAddressOpen(address string) bool {
}
func getProxyHttpClient() *http.Client {
httpProxy := beego.AppConfig.String("httpProxy")
if httpProxy == "" {
sock5Proxy := beego.AppConfig.String("sock5Proxy")
if sock5Proxy == "" {
return &http.Client{}
}
if !isAddressOpen(httpProxy) {
if !isAddressOpen(sock5Proxy) {
return &http.Client{}
}
// https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client
dialer, err := proxy.SOCKS5("tcp", httpProxy, nil, proxy.Direct)
dialer, err := proxy.SOCKS5("tcp", sock5Proxy, nil, proxy.Direct)
if err != nil {
panic(err)
}

View File

@@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/authz"
@@ -57,6 +58,8 @@ func getSubject(ctx *context.Context) (string, string) {
func getObject(ctx *context.Context) (string, string) {
method := ctx.Request.Method
path := ctx.Request.URL.Path
if method == http.MethodGet {
// query == "?id=built-in/admin"
id := ctx.Input.Query("id")
@@ -78,6 +81,14 @@ func getObject(ctx *context.Context) (string, string) {
//panic(err)
return "", ""
}
if path == "/api/delete-resource" {
tokens := strings.Split(obj.Name, "/")
if len(tokens) >= 2 {
obj.Name = tokens[len(tokens)-2]
}
}
return obj.Owner, obj.Name
}
}

View File

@@ -62,7 +62,7 @@ func AutoSigninFilter(ctx *context.Context) {
// "/page?username=abc&password=123"
userId = ctx.Input.Query("username")
password := ctx.Input.Query("password")
if userId != "" && password != "" {
if userId != "" && password != "" && ctx.Input.Query("grant_type") == "" {
owner, name := util.GetOwnerAndNameFromId(userId)
_, msg := object.CheckUserPassword(owner, name, password)
if msg != "" {

View File

@@ -54,7 +54,7 @@ func getUserByClientIdSecret(ctx *context.Context) string {
}
func RecordMessage(ctx *context.Context) {
if ctx.Request.URL.Path == "/api/login" {
if ctx.Request.URL.Path == "/api/login" || ctx.Request.URL.Path == "/api/signup" {
return
}

View File

@@ -127,6 +127,8 @@ func initAPI() {
beego.Router("/api/login/oauth/code", &controllers.ApiController{}, "POST:GetOAuthCode")
beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
beego.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken")
beego.Router("/api/login/oauth/logout", &controllers.ApiController{}, "GET:TokenLogout")
beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")
beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter")
@@ -149,6 +151,12 @@ func initAPI() {
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
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")
beego.Router("/api/add-product", &controllers.ApiController{}, "POST:AddProduct")
beego.Router("/api/delete-product", &controllers.ApiController{}, "POST:DeleteProduct")
beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments")
beego.Router("/api/get-payment", &controllers.ApiController{}, "GET:GetPayment")
beego.Router("/api/update-payment", &controllers.ApiController{}, "POST:UpdatePayment")
@@ -159,5 +167,5 @@ func initAPI() {
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")
beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
beego.Router("/api/certs", &controllers.RootController{}, "*:GetOidcCert")
beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -49,11 +49,11 @@ func ParseBool(s string) bool {
func BoolToString(b bool) string {
if b {
return "1"
} else {
return "0"
}
return "0"
}
//CamelToSnakeCase This function transform camelcase in snakecase LoremIpsum in lorem_ipsum
func CamelToSnakeCase(camel string) string {
var buf bytes.Buffer
for _, c := range camel {
@@ -63,11 +63,11 @@ func CamelToSnakeCase(camel string) string {
buf.WriteRune('_')
}
buf.WriteRune(c - 'A' + 'a')
} else {
buf.WriteRune(c)
continue
}
buf.WriteRune(c)
}
return buf.String()
return strings.ReplaceAll(buf.String(), " ", "")
}
func GetOwnerAndNameFromId(id string) (string, string) {
@@ -124,7 +124,7 @@ func GetMinLenStr(strs ...string) string {
i := 0
for j, str := range strs {
l := len(str)
if l > m {
if l < m {
m = l
i = j
}
@@ -148,23 +148,7 @@ func WriteStringToPath(s string, path string) {
}
}
func ReadBytesFromPath(path string) []byte {
data, err := os.ReadFile(path)
if err != nil {
panic(err)
}
return data
}
func WriteBytesToPath(b []byte, path string) {
err := os.WriteFile(path, b, 0644)
if err != nil {
panic(err)
}
}
// SnakeString XxYy to xx_yy
// SnakeString transform XxYy to xx_yy
func SnakeString(s string) string {
data := make([]byte, 0, len(s)*2)
j := false
@@ -179,7 +163,8 @@ func SnakeString(s string) string {
}
data = append(data, d)
}
return strings.ToLower(string(data[:]))
result := strings.ToLower(string(data[:]))
return strings.ReplaceAll(result, " ", "")
}
func IsChinese(str string) bool {

247
util/string_test.go Normal file
View File

@@ -0,0 +1,247 @@
// Copyright 2021 The casbin 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 util
import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"testing"
)
func TestParseInt(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"Should be return zero when value is empty", "", 0},
{"Should be return 0", "0", 0},
{"Should be return 5", "5", 5},
{"Should be return 10", "10", 10},
{"Should be return -1", "-1", -1},
{"Should be return -5", "-5", -5},
{"Should be return -10", "-10", -10},
{"Should be return -10", "string", "panic"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
if scenery.expected == "panic" {
defer func() {
if r := recover(); r == nil {
t.Error("function should panic")
}
}()
ParseInt(scenery.input)
} else {
actual := ParseInt(scenery.input)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
}
})
}
}
func TestParseBool(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"Should be return false", "0", false},
{"Should be return true", "5", true},
{"Should be return true", "10", true},
{"Should be return true", "-1", true},
{"Should be return false", "", false},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := ParseBool(scenery.input)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestBoolToString(t *testing.T) {
scenarios := []struct {
description string
input bool
expected interface{}
}{
{"Should be return 1", true, "1"},
{"Should be return 0", false, "0"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := BoolToString(scenery.input)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestCamelToSnakeCase(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"Should be return casdor_is_the_best", "CasdoorIsTheBest", "casdoor_is_the_best"},
{"Should be return lorem_ipsum", "Lorem Ipsum", "lorem_ipsum"},
{"Should be return Lorem Ipsum", "lorem Ipsum", "lorem_ipsum"},
{"Should be return lorem_ipsum", "lorem ipsum", "loremipsum"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := CamelToSnakeCase(scenery.input)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestGenerateId(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"Scenery one", GenerateId(), nil},
{"Scenery two", GenerateId(), nil},
{"Scenery three", "00000000-0000-0000-0000-000000000000", nil},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
_, err := uuid.Parse(scenery.input)
assert.Equal(t, scenery.expected, err, "Should be return empty")
})
}
_, SceneryTree := uuid.Parse("00000000-0000-0000-0000-00000000000S")
assert.Equal(t, "invalid UUID format", SceneryTree.Error(), "Errou")
_, SceneryFor := uuid.Parse("00000000-0000-0000-0000-000000000000S")
assert.Equal(t, "invalid UUID length: 37", SceneryFor.Error(), "Errou")
}
func TestGetId(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"Scenery one", "casdoor", "admin/casdoor"},
{"Scenery two", "casbin", "admin/casbin"},
{"Scenery three", "lorem ipsum", "admin/lorem ipsum"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetId(scenery.input)
assert.Equal(t, scenery.expected, actual, "This not is a valid MD5")
})
}
}
func TestGetMd5Hash(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"Scenery one", "casdoor", "0b874f488b4705693a60256b8f3a32da"},
{"Scenery two", "casbin", "59c5a967f086f65366a80dbdd1205a6c"},
{"Scenery three", "lorem ipsum", "80a751fde577028640c419000e33eba6"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetMd5Hash(scenery.input)
assert.Equal(t, scenery.expected, actual, "This not is a valid MD5")
})
}
}
func TestIsStrsEmpty(t *testing.T) {
scenarios := []struct {
description string
input []string
expected interface{}
}{
{"Should be return true if one is empty", []string{"", "lorem", "ipsum"}, true},
{"Should be return true if is empty", []string{""}, true},
{"Should be return false all is a valid string", []string{"lorem", "ipsum"}, false},
{"Should be return false is function called with empty parameters", []string{}, false},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := IsStrsEmpty(scenery.input...)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestGetMaxLenStr(t *testing.T) {
scenarios := []struct {
description string
input []string
expected interface{}
}{
{"Should be return casdoor", []string{"", "casdoor", "casbin"}, "casdoor"},
{"Should be return casdoor_jdk", []string{"", "casdoor", "casbin", "casdoor_jdk"}, "casdoor_jdk"},
{"Should be return empty string", []string{""}, ""},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetMaxLenStr(scenery.input...)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestGetMinLenStr(t *testing.T) {
scenarios := []struct {
description string
input []string
expected interface{}
}{
{"Should be return casbin", []string{"casdoor", "casbin"}, "casbin"},
{"Should be return casbin", []string{"casdoor", "casbin", "casdoor_jdk"}, "casbin"},
{"Should be return empty string", []string{"a", "", "casbin"}, ""},
{"Should be return a", []string{"a", "casdoor", "casbin"}, "a"},
{"Should be return a", []string{"casdoor", "a", "casbin"}, "a"},
{"Should be return a", []string{"casbin", "casdoor", "a"}, "a"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetMinLenStr(scenery.input...)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestSnakeString(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"Should be return casdor_is_the_best", "CasdoorIsTheBest", "casdoor_is_the_best"},
{"Should be return lorem_ipsum", "Lorem Ipsum", "lorem_ipsum"},
{"Should be return lorem_ipsum", "lorem Ipsum", "lorem_ipsum"},
{"Should be return loremipsum", "lorem ipsum", "loremipsum"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := SnakeString(scenery.input)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}

View File

@@ -43,6 +43,9 @@ import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import ProductListPage from "./ProductListPage";
import ProductEditPage from "./ProductEditPage";
import ProductBuyPage from "./ProductBuyPage";
import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage";
import AccountPage from "./account/AccountPage";
@@ -128,6 +131,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/syncers' });
} else if (uri.includes('/certs')) {
this.setState({ selectedMenuKey: '/certs' });
} else if (uri.includes('/products')) {
this.setState({ selectedMenuKey: '/products' });
} else if (uri.includes('/payments')) {
this.setState({ selectedMenuKey: '/payments' });
} else if (uri.includes('/signup')) {
@@ -141,16 +146,14 @@ class App extends Component {
}
}
getAccessTokenParam() {
getAccessTokenParam(params) {
// "/page?access_token=123"
const params = new URLSearchParams(this.props.location.search);
const accessToken = params.get("access_token");
return accessToken === null ? "" : `?accessToken=${accessToken}`;
}
getCredentialParams() {
getCredentialParams(params) {
// "/page?username=abc&password=123"
const params = new URLSearchParams(this.props.location.search);
if (params.get("username") === null || params.get("password") === null) {
return "";
}
@@ -158,8 +161,17 @@ class App extends Component {
}
getUrlWithoutQuery() {
// eslint-disable-next-line no-restricted-globals
return location.toString().replace(location.search, "");
return window.location.toString().replace(window.location.search, "");
}
getLanguageParam(params) {
// "/page?language=en"
const language = params.get("language");
if (language !== null) {
Setting.setLanguage(language);
return `language=${language}`;
}
return "";
}
setLanguage(account) {
@@ -170,13 +182,23 @@ class App extends Component {
}
getAccount() {
let query = this.getAccessTokenParam();
const params = new URLSearchParams(this.props.location.search);
let query = this.getAccessTokenParam(params);
if (query === "") {
query = this.getCredentialParams();
query = this.getCredentialParams(params);
}
const query2 = this.getLanguageParam(params);
if (query2 !== "") {
const url = window.location.toString().replace(new RegExp(`[?&]${query2}`), "");
window.history.replaceState({}, document.title, url);
}
if (query !== "") {
window.history.replaceState({}, document.title, this.getUrlWithoutQuery());
}
AuthBackend.getAccount(query)
.then((res) => {
let account = null;
@@ -412,6 +434,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/products">
<Link to="/products">
{i18next.t("general:Products")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/payments">
<Link to="/payments">
@@ -490,6 +519,9 @@ 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="/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} />)}/>
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>

View File

@@ -47,6 +47,7 @@ class ApplicationEditPage extends React.Component {
certs: [],
providers: [],
uploading: false,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -60,6 +61,9 @@ class ApplicationEditPage extends React.Component {
getApplication() {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
if (application.grantTypes === null || application.grantTypes.length === 0) {
application.grantTypes = ["authorization_code"];
}
this.setState({
application: application,
});
@@ -134,9 +138,10 @@ class ApplicationEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("application:Edit Application")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("application:New Application") : i18next.t("application:Edit Application")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -161,12 +166,12 @@ class ApplicationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Logo", i18next.t("general:Logo - Tooltip"))} :
{Setting.getLabel("general:Logo", i18next.t("general:Logo - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.application.logo} onChange={e => {
@@ -433,6 +438,28 @@ class ApplicationEditPage extends React.Component {
</Popover>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}}
value={this.state.application.grantTypes}
onChange={(value => {
this.updateApplicationField('grantTypes', value);
})} >
{
[
{id: "authorization_code", name: "Authorization Code"},
{id: "password", name: "Password"},
{id: "client_credentials", name: "Client Credentials"},
{id: "token", name: "Token"},
{id: "id_token",name:"ID Token"},
].map((item, index)=><Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :
@@ -492,13 +519,13 @@ class ApplicationEditPage extends React.Component {
if (!Setting.isMobile()) {
return (
<React.Fragment>
<Col span={11} style={{display:'flex',flexDirection:'column'}}>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Col span={11} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Button type="primary">{i18next.t("application:Test signup page..")}</Button>
</a>
<br/>
<br/>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888" ,alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto'}}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
@@ -508,13 +535,13 @@ class ApplicationEditPage extends React.Component {
}
</div>
</Col>
<Col span={11} style={{display:'flex',flexDirection:'column'}}>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signInUrl}>
<Col span={11} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}>
<Button type="primary">{i18next.t("application:Test signin page..")}</Button>
</a>
<br/>
<br/>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888",alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto' }}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
</div>
</Col>
@@ -523,11 +550,11 @@ class ApplicationEditPage extends React.Component {
} else{
return(
<React.Fragment>
<Col span={24} style={{display:'flex',flexDirection:'column'}}>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Col span={24} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Button type="primary">{i18next.t("application:Test signup page..")}</Button>
</a>
<div style={{marginBottom:'10px', width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888" ,alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto'}}>
<div style={{marginBottom:"10px", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
@@ -536,10 +563,10 @@ class ApplicationEditPage extends React.Component {
)
}
</div>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signInUrl}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}>
<Button type="primary">{i18next.t("application:Test signin page..")}</Button>
</a>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888",alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto' }}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
</div>
</Col>
@@ -553,13 +580,13 @@ class ApplicationEditPage extends React.Component {
return (
<React.Fragment>
<Col span={(Setting.isMobile()) ? 24 : 11} style={{display:'flex',flexDirection:'column',flex:'auto'}} >
<a style={{marginBottom: '10px'}} target="_blank" rel="noreferrer" href={promptUrl}>
<Col span={(Setting.isMobile()) ? 24 : 11} style={{display:"flex", flexDirection: "column", flex: "auto"}} >
<a style={{marginBottom: "10px"}} target="_blank" rel="noreferrer" href={promptUrl}>
<Button type="primary">{i18next.t("application:Test prompt page..")}</Button>
</a>
<br style={(Setting.isMobile()) ? {display:'none'} : {}} />
<br style={(Setting.isMobile()) ? {display:'none'} : {}} />
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888",flexDirection:'column',flex:'auto'}}>
<br style={(Setting.isMobile()) ? {display: "none"} : {}} />
<br style={(Setting.isMobile()) ? {display: "none"} : {}} />
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
</div>
</Col>
@@ -592,6 +619,16 @@ class ApplicationEditPage extends React.Component {
});
}
deleteApplication() {
ApplicationBackend.deleteApplication(this.state.application)
.then(() => {
this.props.history.push(`/applications`);
})
.catch(error => {
Setting.showMessage("error", `Application failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -601,6 +638,7 @@ class ApplicationEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -34,7 +34,7 @@ class ApplicationListPage extends BaseListPage {
logo: "https://cdn.casdoor.com/logo/casdoor-logo_1185x256.png",
enablePassword: true,
enableSignUp: true,
enableSigninSession: true,
enableSigninSession: false,
enableCodeSignin: false,
providers: [],
signupItems: [
@@ -58,8 +58,7 @@ class ApplicationListPage extends BaseListPage {
const newApplication = this.newApplication();
ApplicationBackend.addApplication(newApplication)
.then((res) => {
Setting.showMessage("success", `Application added successfully`);
this.props.history.push(`/applications/${newApplication.name}`);
this.props.history.push({pathname: `/applications/${newApplication.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -30,6 +30,7 @@ class CertEditPage extends React.Component {
classes: props,
certName: props.match.params.certName,
cert: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -67,9 +68,10 @@ class CertEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("cert:New Cert") : i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -236,6 +238,16 @@ class CertEditPage extends React.Component {
});
}
deleteCert() {
CertBackend.deleteCert(this.state.cert)
.then(() => {
this.props.history.push(`/certs`);
})
.catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -245,6 +257,7 @@ class CertEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -44,8 +44,7 @@ class CertListPage extends BaseListPage {
const newCert = this.newCert();
CertBackend.addCert(newCert)
.then((res) => {
Setting.showMessage("success", `Cert added successfully`);
this.props.history.push(`/certs/${newCert.name}`);
this.props.history.push({pathname: `/certs/${newCert.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -26,7 +26,7 @@ class LdapSyncPage extends React.Component {
ldap: null,
users: [],
existUuids: [],
selectedUsers: []
selectedUsers: [],
};
}
@@ -212,7 +212,7 @@ class LdapSyncPage extends React.Component {
return (
<div>
<Table rowSelection={rowSelection} columns={columns} dataSource={users} rowKey="uuid" bordered
pagination={{pageSize: 100}}
pagination={{defaultPageSize: 10, showQuickJumper: true, showSizeChanger: true}}
title={() => (
<div>
<span>{this.state.ldap?.serverName}</span>

View File

@@ -31,6 +31,7 @@ class OrganizationEditPage extends React.Component {
organizationName: props.match.params.organizationName,
organization: null,
ldaps: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -84,9 +85,10 @@ class OrganizationEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("organization:Edit Organization")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("organization:New Organization") : i18next.t("organization:Edit Organization")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -111,12 +113,12 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Favicon", i18next.t("general:Favicon - Tooltip"))} :
{Setting.getLabel("general:Favicon", i18next.t("general:Favicon - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.organization.favicon} onChange={e => {
@@ -186,7 +188,7 @@ class OrganizationEditPage extends React.Component {
<Col span={22} >
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.organization.defaultAvatar} onChange={e => {
@@ -269,6 +271,16 @@ class OrganizationEditPage extends React.Component {
});
}
deleteOrganization() {
OrganizationBackend.deleteOrganization(this.state.organization)
.then(() => {
this.props.history.push(`/organizations`);
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
@@ -278,6 +290,7 @@ class OrganizationEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -45,8 +45,7 @@ class OrganizationListPage extends BaseListPage {
const newOrganization = this.newOrganization();
OrganizationBackend.addOrganization(newOrganization)
.then((res) => {
Setting.showMessage("success", `Organization added successfully`);
this.props.history.push(`/organizations/${newOrganization.name}`);
this.props.history.push({pathname: `/organizations/${newOrganization.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -13,15 +13,10 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
import {Button, Card, Col, Input, Row} from 'antd';
import * as PaymentBackend from "./backend/PaymentBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend";
const { Option } = Select;
class PaymentEditPage extends React.Component {
constructor(props) {
@@ -31,6 +26,7 @@ class PaymentEditPage extends React.Component {
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
paymentName: props.match.params.paymentName,
payment: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -68,9 +64,10 @@ class PaymentEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("payment:Edit Payment")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("payment:New Payment") : i18next.t("payment:Edit Payment")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -103,16 +100,6 @@ class PaymentEditPage extends React.Component {
}} />
</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.payment.name} onChange={e => {
// this.updatePaymentField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} :
@@ -192,6 +179,16 @@ class PaymentEditPage extends React.Component {
});
}
deletePayment() {
PaymentBackend.deletePayment(this.state.payment)
.then(() => {
this.props.history.push(`/payments`);
})
.catch(error => {
Setting.showMessage("error", `Payment failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -201,6 +198,7 @@ class PaymentEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd';
import {Button, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as PaymentBackend from "./backend/PaymentBackend";
@@ -45,8 +45,7 @@ class PaymentListPage extends BaseListPage {
const newPayment = this.newPayment();
PaymentBackend.addPayment(newPayment)
.then((res) => {
Setting.showMessage("success", `Payment added successfully`);
this.props.history.push(`/payments/${newPayment.name}`);
this.props.history.push({pathname: `/payments/${newPayment.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -34,6 +34,7 @@ class PermissionEditPage extends React.Component {
organizations: [],
users: [],
roles: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -102,9 +103,10 @@ class PermissionEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("permission:Edit Permission")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("permission:New Permission") : i18next.t("permission:Edit Permission")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -258,6 +260,16 @@ class PermissionEditPage extends React.Component {
});
}
deletePermission() {
PermissionBackend.deletePermission(this.state.permission)
.then(() => {
this.props.history.push(`/permissions`);
})
.catch(error => {
Setting.showMessage("error", `Permission failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -267,6 +279,7 @@ class PermissionEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -43,8 +43,7 @@ class PermissionListPage extends BaseListPage {
const newPermission = this.newPermission();
PermissionBackend.addPermission(newPermission)
.then((res) => {
Setting.showMessage("success", `Permission added successfully`);
this.props.history.push(`/permissions/${newPermission.owner}/${newPermission.name}`);
this.props.history.push({pathname: `/permissions/${newPermission.owner}/${newPermission.name}`, mode: "add"});
}
)
.catch(error => {

196
web/src/ProductBuyPage.js Normal file
View File

@@ -0,0 +1,196 @@
// 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.
import React from "react";
import {Button, Descriptions} from "antd";
import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend";
import * as ProviderBackend from "./backend/ProviderBackend";
import * as Provider from "./auth/Provider";
class ProductBuyPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
productName: props.match?.params.productName,
product: null,
providers: [],
};
}
UNSAFE_componentWillMount() {
this.getProduct();
this.getPaymentProviders();
}
getProduct() {
ProductBackend.getProduct("admin", this.state.productName)
.then((product) => {
this.setState({
product: product,
});
});
}
getPaymentProviders() {
ProviderBackend.getProviders("admin")
.then((res) => {
this.setState({
providers: res.filter(provider => provider.category === "Payment"),
});
});
}
getProductObj() {
if (this.props.product !== undefined) {
return this.props.product;
} else {
return this.state.product;
}
}
getCurrencySymbol(product) {
if (product?.currency === "USD") {
return "$";
} else if (product?.currency === "CNY") {
return "¥";
} else {
return "(Unknown currency)";
}
}
getCurrencyText(product) {
if (product?.currency === "USD") {
return i18next.t("product:USD");
} else if (product?.currency === "CNY") {
return i18next.t("product:CNY");
} else {
return "(Unknown currency)";
}
}
getProviders(product) {
if (this.state.providers.length === 0 || product.providers.length === 0) {
return [];
}
let providerMap = {};
this.state.providers.forEach(provider => {
providerMap[provider.name] = provider;
})
return product.providers.map(providerName => providerMap[providerName]);
}
getPayUrl(product, provider) {
if (product === null || provider === null) {
return "";
}
return `https://${provider.type}`;
// if (provider.type === "WeChat") {
// return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
// } else if (provider.type === "GitHub") {
// return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
// }
}
getPayButton(provider) {
let text = provider.type;
if (provider.type === "Alipay") {
text = i18next.t("product:Alipay");
} else if (provider.type === "WeChat Pay") {
text = i18next.t("product:WeChat Pay");
} else if (provider.type === "Paypal") {
text = i18next.t("product:Paypal");
}
return (
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
<img style={{marginRight: "10px"}} width={36} height={36} src={Provider.getProviderLogo(provider)} alt={provider.displayName} />
} size={"large"} >
{
text
}
</Button>
)
}
renderProviderButton(provider, product) {
return (
<span key={provider.name} style={{width: "200px", marginRight: "20px", marginBottom: "10px"}}>
<a style={{width: "200px"}} href={this.getPayUrl(product, provider)}>
{
this.getPayButton(provider)
}
</a>
</span>
)
}
renderPay(product) {
if (product === undefined || product === null) {
return null;
}
if (product.state !== "Published") {
return i18next.t("product:This product is currently not in sale.");
}
if (product.providers.length === 0) {
return i18next.t("product:There is no payment channel for this product.");
}
const providers = this.getProviders(product);
return providers.map(provider => {
return this.renderProviderButton(provider, product);
})
}
render() {
const product = this.getProductObj();
return (
<div>
<Descriptions title={i18next.t("product:Buy Product")} bordered>
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
<span style={{fontSize: 28}}>
{product?.displayName}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Detail")}><span style={{fontSize: 16}}>{product?.detail}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
<img src={product?.image} alt={product?.image} height={90} style={{marginBottom: '20px'}}/>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
{`${this.getCurrencySymbol(product)}${product?.price} (${this.getCurrencyText(product)})`}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Quantity")}><span style={{fontSize: 16}}>{product?.quantity}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Sold")}><span style={{fontSize: 16}}>{product?.sold}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
{
this.renderPay(product)
}
</Descriptions.Item>
</Descriptions>
</div>
)
}
}
export default ProductBuyPage;

311
web/src/ProductEditPage.js Normal file
View File

@@ -0,0 +1,311 @@
// 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.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd';
import * as ProductBackend from "./backend/ProductBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
import * as ProviderBackend from "./backend/ProviderBackend";
import ProductBuyPage from "./ProductBuyPage";
const { Option } = Select;
class ProductEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
productName: props.match.params.productName,
product: null,
providers: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getProduct();
this.getPaymentProviders();
}
getProduct() {
ProductBackend.getProduct("admin", this.state.productName)
.then((product) => {
this.setState({
product: product,
});
});
}
getPaymentProviders() {
ProviderBackend.getProviders("admin")
.then((res) => {
this.setState({
providers: res.filter(provider => provider.category === "Payment"),
});
});
}
parseProductField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateProductField(key, value) {
value = this.parseProductField(key, value);
let product = this.state.product;
product[key] = value;
this.setState({
product: product,
});
}
renderProduct() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("product:New Product") : i18next.t("product:Edit Product")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<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.product.name} onChange={e => {
this.updateProductField('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.product.displayName} onChange={e => {
this.updateProductField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.product.image} onChange={e => {
this.updateProductField('image', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}:
</Col>
<Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.product.image}>
<img src={this.state.product.image} alt={this.state.product.image} height={90} style={{marginBottom: '20px'}}/>
</a>
</Col>
</Row>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.tag} onChange={e => {
this.updateProductField('tag', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Detail"), i18next.t("product:Detail - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.detail} onChange={e => {
this.updateProductField('detail', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Currency"), i18next.t("product:Currency - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.currency} onChange={(value => {
this.updateProductField('currency', value);
})}>
{
[
{id: 'USD', name: 'USD'},
{id: 'CNY', name: 'CNY'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.price} onChange={value => {
this.updateProductField('price', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.quantity} onChange={value => {
this.updateProductField('quantity', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.sold} onChange={value => {
this.updateProductField('sold', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.product.providers} onChange={(value => {this.updateProductField('providers', value);})}>
{
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.state} onChange={(value => {
this.updateProductField('state', value);
})}>
{
[
{id: 'Published', name: 'Published'},
{id: 'Draft', name: 'Draft'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
{
this.renderPreview()
}
</Row>
</Card>
)
}
renderPreview() {
let buyUrl = `/products/${this.state.product.name}/buy`;
return (
<Col span={22} style={{display: "flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={buyUrl}>
<Button type="primary">{i18next.t("product:Test buy page..")}</Button>
</a>
<br/>
<br/>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<ProductBuyPage product={this.state.product} />
</div>
</Col>
)
}
submitProductEdit(willExist) {
let product = Setting.deepCopy(this.state.product);
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
productName: this.state.product.name,
});
if (willExist) {
this.props.history.push(`/products`);
} else {
this.props.history.push(`/products/${this.state.product.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateProductField('name', this.state.productName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
deleteProduct() {
ProductBackend.deleteProduct(this.state.product)
.then(() => {
this.props.history.push(`/products`);
})
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
});
}
render() {
return (
<div>
{
this.state.product !== null ? this.renderProduct() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default ProductEditPage;

295
web/src/ProductListPage.js Normal file
View File

@@ -0,0 +1,295 @@
// 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.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as ProductBackend from "./backend/ProductBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import {EditOutlined} from "@ant-design/icons";
class ProductListPage extends BaseListPage {
newProduct() {
const randomName = Setting.getRandomName();
return {
owner: "admin",
name: `product_${randomName}`,
createdTime: moment().format(),
displayName: `New Product - ${randomName}`,
image: "https://cdn.casdoor.com/logo/casdoor-logo_1185x256.png",
tag: "Casdoor Summit 2022",
currency: "USD",
price: 300,
quantity: 99,
sold: 10,
providers: [],
state: "Published",
}
}
addProduct() {
const newProduct = this.newProduct();
ProductBackend.addProduct(newProduct)
.then((res) => {
this.props.history.push({pathname: `/products/${newProduct.name}`, mode: "add"});
}
)
.catch(error => {
Setting.showMessage("error", `Product failed to add: ${error}`);
});
}
deleteProduct(i) {
ProductBackend.deleteProduct(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Product deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
});
}
renderTable(products) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '140px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/products/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
width: '170px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("product:Image"),
dataIndex: 'image',
key: 'image',
width: '170px',
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={150} />
</a>
)
}
},
{
title: i18next.t("product:Tag"),
dataIndex: 'tag',
key: 'tag',
width: '160px',
sorter: true,
...this.getColumnSearchProps('tag'),
},
{
title: i18next.t("product:Currency"),
dataIndex: 'currency',
key: 'currency',
width: '120px',
sorter: true,
...this.getColumnSearchProps('currency'),
},
{
title: i18next.t("product:Price"),
dataIndex: 'price',
key: 'price',
width: '120px',
sorter: true,
...this.getColumnSearchProps('price'),
},
{
title: i18next.t("product:Quantity"),
dataIndex: 'quantity',
key: 'quantity',
width: '120px',
sorter: true,
...this.getColumnSearchProps('quantity'),
},
{
title: i18next.t("product:Sold"),
dataIndex: 'sold',
key: 'sold',
width: '120px',
sorter: true,
...this.getColumnSearchProps('sold'),
},
{
title: i18next.t("general:State"),
dataIndex: 'state',
key: 'state',
width: '120px',
sorter: true,
...this.getColumnSearchProps('state'),
},
{
title: i18next.t("product:Payment providers"),
dataIndex: 'providers',
key: 'providers',
width: '500px',
...this.getColumnSearchProps('providers'),
render: (text, record, index) => {
const providers = text;
if (providers.length === 0) {
return "(empty)";
}
const half = Math.floor((providers.length + 1) / 2);
const getList = (providers) => {
return (
<List
size="small"
locale={{emptyText: " "}}
dataSource={providers}
renderItem={(providerName, i) => {
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title="Edit">
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/providers/${providerName}`)} />
</Tooltip>
<Link to={`/providers/${providerName}`}>
{providerName}
</Link>
</div>
</List.Item>
)
}}
/>
)
}
return (
<div>
<Row>
<Col span={12}>
{
getList(providers.slice(0, half))
}
</Col>
<Col span={12}>
{
getList(providers.slice(half))
}
</Col>
</Row>
</div>
)
},
},
{
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(`/products/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete product: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</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={products} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Products")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addProduct.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;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
ProductBackend.getProducts("", 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,
});
}
});
};
}
export default ProductListPage;

View File

@@ -31,6 +31,7 @@ class ProviderEditPage extends React.Component {
classes: props,
providerName: props.match.params.providerName,
provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -133,9 +134,10 @@ class ProviderEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("provider:Edit Provider")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("provider:New Provider") : i18next.t("provider:Edit Provider")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitProviderEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitProviderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteProvider()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -293,6 +295,20 @@ class ProviderEditPage extends React.Component {
</React.Fragment>
)
}
{
this.state.provider.type !== "Adfs" ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.domain} onChange={e => {
this.updateProviderField('domain', e.target.value);
}} />
</Col>
</Row>
)
}
{this.state.provider.category === "Storage" ? (
<div>
<Row style={{marginTop: '20px'}} >
@@ -558,6 +574,16 @@ class ProviderEditPage extends React.Component {
});
}
deleteProvider() {
ProviderBackend.deleteProvider(this.state.provider)
.then(() => {
this.props.history.push(`/providers`);
})
.catch(error => {
Setting.showMessage("error", `Provider failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -567,6 +593,7 @@ class ProviderEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitProviderEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitProviderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteProvider()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -47,8 +47,7 @@ class ProviderListPage extends BaseListPage {
const newProvider = this.newProvider();
ProviderBackend.addProvider(newProvider)
.then((res) => {
Setting.showMessage("success", `Provider added successfully`);
this.props.history.push(`/providers/${newProvider.name}`);
this.props.history.push({pathname: `/providers/${newProvider.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -74,7 +74,7 @@ class ResourceListPage extends BaseListPage {
renderUpload() {
return (
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf" showUploadList={false}
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx" showUploadList={false}
beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}>
<Button icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
{i18next.t("resource:Upload a file...")}

View File

@@ -33,6 +33,7 @@ class RoleEditPage extends React.Component {
organizations: [],
users: [],
roles: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -101,9 +102,10 @@ class RoleEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("role:Edit Role")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("role:New Role") : i18next.t("role:Edit Role")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitRoleEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitRoleEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteRole()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -201,6 +203,16 @@ class RoleEditPage extends React.Component {
});
}
deleteRole() {
RoleBackend.deleteRole(this.state.role)
.then(() => {
this.props.history.push(`/roles`);
})
.catch(error => {
Setting.showMessage("error", `Role failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -210,6 +222,7 @@ class RoleEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitRoleEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitRoleEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteRole()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -39,8 +39,7 @@ class RoleListPage extends BaseListPage {
const newRole = this.newRole();
RoleBackend.addRole(newRole)
.then((res) => {
Setting.showMessage("success", `Role added successfully`);
this.props.history.push(`/roles/${newRole.owner}/${newRole.name}`);
this.props.history.push({pathname: `/roles/${newRole.owner}/${newRole.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -325,6 +325,10 @@ export function getAvatarColor(s) {
return colorList[random % 4];
}
export function getLanguage() {
return i18next.language;
}
export function setLanguage(language) {
localStorage.setItem("language", language);
changeMomentLanguage(language);
@@ -398,11 +402,13 @@ export function getProviderTypeOptions(category) {
{id: 'WeCom', name: 'WeCom'},
{id: 'Lark', name: 'Lark'},
{id: 'GitLab', name: 'GitLab'},
{id: 'Adfs', name: 'Adfs'},
{id: 'Baidu', name: 'Baidu'},
{id: 'Infoflow', name: 'Infoflow'},
{id: 'Apple', name: 'Apple'},
{id: 'AzureAD', name: 'AzureAD'},
{id: 'Slack', name: 'Slack'},
{id: 'Steam', name: 'Steam'},
]
);
} else if (category === "Email") {

View File

@@ -170,7 +170,8 @@ class SignupTable extends React.Component {
} if (record.name === "Display name") {
options = [
{id: 'None', name: 'None'},
{id: 'Personal', name: 'Personal'},
{id: 'Real name', name: 'Real name'},
{id: 'First, last', name: 'First, last'},
];
}

View File

@@ -36,6 +36,7 @@ class SyncerEditPage extends React.Component {
syncerName: props.match.params.syncerName,
syncer: null,
organizations: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -83,9 +84,10 @@ class SyncerEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("syncer:Edit Syncer")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("syncer:New Syncer") : i18next.t("syncer:Edit Syncer")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitSyncerEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitSyncerEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteSyncer()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -308,6 +310,16 @@ class SyncerEditPage extends React.Component {
});
}
deleteSyncer() {
SyncerBackend.deleteSyncer(this.state.syncer)
.then(() => {
this.props.history.push(`/syncers`);
})
.catch(error => {
Setting.showMessage("error", `Syncer failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -317,6 +329,7 @@ class SyncerEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitSyncerEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitSyncerEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteSyncer()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -50,8 +50,7 @@ class SyncerListPage extends BaseListPage {
const newSyncer = this.newSyncer();
SyncerBackend.addSyncer(newSyncer)
.then((res) => {
Setting.showMessage("success", `Syncer added successfully`);
this.props.history.push(`/syncers/${newSyncer.name}`);
this.props.history.push({pathname: `/syncers/${newSyncer.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -25,6 +25,7 @@ class TokenEditPage extends React.Component {
classes: props,
tokenName: props.match.params.tokenName,
token: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -62,9 +63,10 @@ class TokenEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("token:Edit Token")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("token:New Token") : i18next.t("token:Edit Token")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitTokenEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitTokenEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteToken()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -186,6 +188,16 @@ class TokenEditPage extends React.Component {
});
}
deleteToken() {
TokenBackend.deleteToken(this.state.token)
.then(() => {
this.props.history.push(`/tokens`);
})
.catch(error => {
Setting.showMessage("error", `Token failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -195,6 +207,7 @@ class TokenEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitTokenEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitTokenEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteToken()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -44,8 +44,7 @@ class TokenListPage extends BaseListPage {
const newToken = this.newToken();
TokenBackend.addToken(newToken)
.then((res) => {
Setting.showMessage("success", `Token added successfully`);
this.props.history.push(`/tokens/${newToken.name}`);
this.props.history.push({pathname: `/tokens/${newToken.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -46,6 +46,7 @@ class UserEditPage extends React.Component {
application: null,
organizations: [],
applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -121,9 +122,10 @@ class UserEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("user:New User") : i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -430,6 +432,16 @@ class UserEditPage extends React.Component {
});
}
deleteUser() {
UserBackend.deleteUser(this.state.user)
.then(() => {
this.props.history.push(`/users`);
})
.catch(error => {
Setting.showMessage("error", `User failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -439,6 +451,7 @@ class UserEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -21,6 +21,7 @@ import * as Setting from "./Setting";
import * as UserBackend from "./backend/UserBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import * as path from "path";
class UserListPage extends BaseListPage {
constructor(props) {
@@ -69,8 +70,7 @@ class UserListPage extends BaseListPage {
const newUser = this.newUser();
UserBackend.addUser(newUser)
.then((res) => {
Setting.showMessage("success", `User added successfully`);
this.props.history.push(`/users/${newUser.owner}/${newUser.name}`);
this.props.history.push({pathname: `/users/${newUser.owner}/${newUser.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -96,6 +96,7 @@ class WebhookEditPage extends React.Component {
webhookName: props.match.params.webhookName,
webhook: null,
organizations: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -149,9 +150,10 @@ class WebhookEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("webhook:Edit Webhook")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("webhook:New Webhook") : i18next.t("webhook:Edit Webhook")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitWebhookEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitWebhookEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteWebhook()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@@ -315,6 +317,16 @@ class WebhookEditPage extends React.Component {
});
}
deleteWebhook() {
WebhookBackend.deleteWebhook(this.state.webhook)
.then(() => {
this.props.history.push(`/webhooks`);
})
.catch(error => {
Setting.showMessage("error", `Webhook failed to delete: ${error}`);
});
}
render() {
return (
<div>
@@ -324,6 +336,7 @@ class WebhookEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitWebhookEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitWebhookEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteWebhook()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@@ -42,8 +42,7 @@ class WebhookListPage extends BaseListPage {
const newWebhook = this.newWebhook();
WebhookBackend.addWebhook(newWebhook)
.then((res) => {
Setting.showMessage("success", `Webhook added successfully`);
this.props.history.push(`/webhooks/${newWebhook.name}`);
this.props.history.push({pathname: `/webhooks/${newWebhook.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -18,7 +18,7 @@ import UserEditPage from "../UserEditPage";
class AccountPage extends React.Component {
render() {
return (
<UserEditPage organizationName={this.props.account.owner} userName={this.props.account.name} account={this.props.account} />
<UserEditPage organizationName={this.props.account.owner} userName={this.props.account.name} account={this.props.account} location={this.props.location} />
)
}
}

View File

@@ -0,0 +1,32 @@
// 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.
import {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";
function Icon({ width = 24, height = 24, color }) {
return <img src={`${StaticBaseUrl}/buttons/adfs.svg`} alt="Sign in with ADFS" style={{width: 24, height: 24}} />;
}
const config = {
text: "Sign in with ADFS",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const AdfsLoginButton = createButton(config);
export default AdfsLoginButton;

View File

@@ -58,6 +58,10 @@ class AuthCallback extends React.Component {
if (authServerUrl === realRedirectUrl) {
return "login";
} else {
const responseType = innerParams.get("response_type");
if (responseType !== null) {
return responseType
}
return "code";
}
} else if (method === "link") {
@@ -69,6 +73,7 @@ class AuthCallback extends React.Component {
UNSAFE_componentWillMount() {
const params = new URLSearchParams(this.props.location.search);
let isSteam = params.get("openid.mode")
let code = params.get("code");
// WeCom returns "auth_code=xxx" instead of "code=xxx"
if (code === null) {
@@ -78,6 +83,10 @@ class AuthCallback extends React.Component {
if (code === null) {
code = params.get("authCode")
}
//Steam don't use code, so we should use all params as code.
if (isSteam !== null && code === null) {
code = this.props.location.search
}
const innerParams = this.getInnerParams();
const applicationName = innerParams.get("application");
@@ -111,6 +120,9 @@ class AuthCallback extends React.Component {
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}?code=${code}&state=${oAuthParams.state}`);
// Util.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token"){
const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}?${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "link") {
const from = innerParams.get("from");
Setting.goToLinkSoft(this, from);

View File

@@ -34,11 +34,13 @@ import LinkedInLoginButton from "./LinkedInLoginButton";
import WeComLoginButton from "./WeComLoginButton";
import LarkLoginButton from "./LarkLoginButton";
import GitLabLoginButton from "./GitLabLoginButton";
import AdfsLoginButton from "./AdfsLoginButton";
import BaiduLoginButton from "./BaiduLoginButton";
import InfoflowLoginButton from "./InfoflowLoginButton";
import AppleLoginButton from "./AppleLoginButton"
import AzureADLoginButton from "./AzureADLoginButton";
import SlackLoginButton from "./SlackLoginButton";
import SteamLoginButton from "./SteamLoginButton";
import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput";
@@ -114,14 +116,18 @@ class LoginPage extends React.Component {
onFinish(values) {
const application = this.getApplicationObj();
const ths = this;
values["type"] = this.state.type;
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
const oAuthParams = Util.getOAuthGetParameters();
if (oAuthParams !== null && oAuthParams.responseType!= null && oAuthParams.responseType !== "") {
values["type"] = oAuthParams.responseType
}else{
values["type"] = this.state.type;
}
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
AuthBackend.login(values, oAuthParams)
.then((res) => {
if (res.status === 'ok') {
const responseType = this.state.type;
const responseType = values["type"];
if (responseType === "login") {
Util.showMessage("success", `Logged in successfully`);
@@ -154,6 +160,9 @@ class LoginPage extends React.Component {
}
// Util.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
}
} else {
Util.showMessage("error", `Failed to log in: ${res.msg}`);
@@ -187,6 +196,8 @@ class LoginPage extends React.Component {
return <LarkLoginButton text={text} align={"center"} />
} else if (type === "GitLab") {
return <GitLabLoginButton text={text} align={"center"} />
} else if (type === "Adfs") {
return <AdfsLoginButton text={text} align={"center"} />
} else if (type === "Baidu") {
return <BaiduLoginButton text={text} align={"center"} />
} else if (type === "Infoflow") {
@@ -197,6 +208,8 @@ class LoginPage extends React.Component {
return <AzureADLoginButton text={text} align={"center"} />
} else if (type === "Slack") {
return <SlackLoginButton text={text} align={"center"} />
} else if (type === "Steam") {
return <SteamLoginButton text={text} align={"center"} />
}
return text;

View File

@@ -70,6 +70,10 @@ const authInfo = {
scope: "read_user+profile",
endpoint: "https://gitlab.com/oauth/authorize",
},
Adfs: {
scope: "openid",
endpoint: "http://example.com",
},
Baidu: {
scope: "basic",
endpoint: "http://openapi.baidu.com/oauth/2.0/authorize",
@@ -89,6 +93,9 @@ const authInfo = {
scope: "users:read",
endpoint: "https://slack.com/oauth/authorize",
},
Steam: {
endpoint: "https://steamcommunity.com/openid/login",
},
};
const otherProviderInfo = {
@@ -264,6 +271,8 @@ export function getAuthUrl(application, provider, method) {
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`;
} else if (provider.type === "GitLab") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Adfs") {
return `${provider.domain}/adfs/oauth2/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&nonce=casdoor&scope=openid`;
} else if (provider.type === "Baidu") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&display=popup`;
} else if (provider.type === "Infoflow"){
@@ -274,5 +283,7 @@ export function getAuthUrl(application, provider, method) {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&resource=https://graph.windows.net/`;
} else if (provider.type === "Slack") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Steam") {
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`;
}
}

View File

@@ -192,15 +192,50 @@ class SignupPage extends React.Component {
</Form.Item>
)
} else if (signupItem.name === "Display name") {
if (signupItem.rule === "First, last" && Setting.getLanguage() !== "zh") {
return (
<React.Fragment>
<Form.Item
name="firstName"
key="firstName"
label={i18next.t("general:First name")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your first name!"),
whitespace: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="lastName"
key="lastName"
label={i18next.t("general:Last name")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your last name!"),
whitespace: true,
},
]}
>
<Input />
</Form.Item>
</React.Fragment>
)
}
return (
<Form.Item
name="name"
key="name"
label={signupItem.rule === "Personal" ? i18next.t("general:Personal name") : i18next.t("general:Display name")}
label={(signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("general:Real name") : i18next.t("general:Display name")}
rules={[
{
required: required,
message: signupItem.rule === "Personal" ? i18next.t("signup:Please input your personal name!") : i18next.t("signup:Please input your display name!"),
message: (signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("signup:Please input your real name!") : i18next.t("signup:Please input your display name!"),
whitespace: true,
},
]}

View File

@@ -0,0 +1,32 @@
// Copyright 2022 The casbin 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 {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";
function Icon({ width = 24, height = 24, color }) {
return <img src={`${StaticBaseUrl}/buttons/steam.svg`} alt="Sign in with Steam" style={{width: 24, height: 24}} />;
}
const config = {
text: "Sign in with Steam",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const SteamLoginButton = createButton(config);
export default SteamLoginButton;

View File

@@ -0,0 +1,56 @@
// 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.
import * as Setting from "../Setting";
export function getProducts(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-products?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getProduct(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-product?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateProduct(owner, name, product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/update-product?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}
export function addProduct(product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/add-product`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}
export function deleteProduct(product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/delete-product`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}

View File

@@ -14,6 +14,9 @@
"Enable signup": "Anmeldung aktivieren",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Passwort AN",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei",
@@ -45,6 +48,7 @@
"Edit Cert": "Zitat bearbeiten",
"Expire in years": "Gültig in Jahren",
"Expire in years - Tooltip": "Verfällt in Jahren - Tooltip",
"New Cert": "New Cert",
"Private key": "Privater Schlüssel",
"Private key - Tooltip": "Privater Schlüssel - Tooltip",
"Private key copied to clipboard successfully": "Privater Schlüssel erfolgreich in die Zwischenablage kopiert",
@@ -91,6 +95,7 @@
"Avatar": "Avatar",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Zurück zu Hause",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@@ -109,6 +114,7 @@
"Email": "E-Mail",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URL vergessen",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Zuhause",
@@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Ist aktiviert - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Master-Passwort",
"Master password - Tooltip": "Masterpasswort - Tooltip",
@@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Berechtigungen",
"Personal name": "Persönlicher Name",
"Phone": "Telefon",
"Phone - Tooltip": "Phone",
"Phone prefix": "Telefonpräfix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Vorschau",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Anbieter",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Anbieter",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Persönlicher Name",
"Records": "Datensätze",
"Request URI": "Request URI",
"Resources": "Ressourcen",
@@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Zeitstempel",
"Tokens": "Token",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Hoch",
"User": "Benutzer",
"User - Tooltip": "Benutzer - Tooltip",
@@ -233,6 +244,7 @@
"Default avatar": "Standard Avatar",
"Edit Organization": "Organisation bearbeiten",
"Favicon": "Févicon",
"New Organization": "New Organization",
"Soft deletion": "Weiche Löschung",
"Soft deletion - Tooltip": "Weiche Löschung - Tooltip",
"Website URL": "Website-URL",
@@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Aktionen",
@@ -253,10 +266,42 @@
"Edit Permission": "Berechtigung bearbeiten",
"Effect": "Effekt",
"Effect - Tooltip": "Effekt - Tooltip",
"New Permission": "New Permission",
"Resource type": "Ressourcentyp",
"Resource type - Tooltip": "Ressourcentyp - Tooltip",
"Resources": "Ressourcen"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Zugangsschlüssel",
"Access key - Tooltip": "Zugriffsschlüssel - Tooltip",
@@ -299,6 +344,7 @@
"Method": "Methode",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Name",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Metadaten erfolgreich analysieren",
"Port": "Port",
@@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Rolle bearbeiten",
"New Role": "New Role",
"Sub roles": "Unterrollen",
"Sub roles - Tooltip": "Unterrollen - Tooltip",
"Sub users": "Unternutzer",
@@ -381,8 +428,10 @@
"Please input your address!": "Bitte geben Sie Ihre Adresse ein!",
"Please input your affiliation!": "Bitte geben Sie Ihre Zugehörigkeit ein!",
"Please input your display name!": "Bitte geben Sie Ihren Anzeigenamen ein!",
"Please input your personal name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Bitte geben Sie Ihre Telefonnummer ein!",
"Please input your real name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region!",
"Terms of Use": "Nutzungsbedingungen",
"The input is not valid Email!": "Die Eingabe ist ungültig!",
@@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Ist gehasht",
"New Syncer": "New Syncer",
"Sync interval": "Sync-Intervall",
"Sync interval - Tooltip": "Sync-Intervall - Tooltip",
"Table": "Tisch",
@@ -424,6 +474,7 @@
"Authorization code": "Autorisierungscode",
"Edit Token": "Token bearbeiten",
"Expires in": "Läuft ab",
"New Token": "New Token",
"Scope": "Bereich",
"Token type": "Token-Typ"
},
@@ -462,6 +513,7 @@
"Modify password...": "Passwort ändern...",
"New Email": "Neue E-Mail",
"New Password": "Neues Passwort",
"New User": "New User",
"New phone": "Neues Telefon",
"OK": "Ok",
"Old Password": "Altes Passwort",
@@ -498,6 +550,7 @@
"Method": "Methode",
"Method - Tooltip": "Methode - Tooltip",
"Name": "Name",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Value": "Wert"

View File

@@ -14,6 +14,9 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Enable signup - Tooltip",
"File uploaded successfully": "File uploaded successfully",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Password ON",
"Password ON - Tooltip": "Password ON - Tooltip",
"Please select a HTML file": "Please select a HTML file",
@@ -45,6 +48,7 @@
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
@@ -91,6 +95,7 @@
"Avatar": "Avatar",
"Avatar - Tooltip": "Avatar - Tooltip",
"Back Home": "Back Home",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@@ -109,6 +114,7 @@
"Email": "Email",
"Email - Tooltip": "Email - Tooltip",
"Favicon - Tooltip": "Favicon - Tooltip",
"First name": "First name",
"Forget URL": "Forget URL",
"Forget URL - Tooltip": "Forget URL - Tooltip",
"Home": "Home",
@@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Is enabled - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "Logo - Tooltip",
"Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip",
@@ -137,17 +144,18 @@
"Password type - Tooltip": "Password type - Tooltip",
"Payments": "Payments",
"Permissions": "Permissions",
"Personal name": "Personal name",
"Phone": "Phone",
"Phone - Tooltip": "Phone - Tooltip",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Phone prefix - Tooltip",
"Preview": "Preview",
"Preview - Tooltip": "Preview - Tooltip",
"Products": "Products",
"Provider": "Provider",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Providers",
"Providers - Tooltip": "Providers - Tooltip",
"Real name": "Real name",
"Records": "Records",
"Request URI": "Request URI",
"Resources": "Resources",
@@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Up",
"User": "User",
"User - Tooltip": "User - Tooltip",
@@ -233,6 +244,7 @@
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
"New Organization": "New Organization",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
"Website URL": "Website URL",
@@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Actions",
@@ -253,10 +266,42 @@
"Edit Permission": "Edit Permission",
"Effect": "Effect",
"Effect - Tooltip": "Effect - Tooltip",
"New Permission": "New Permission",
"Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
@@ -299,6 +344,7 @@
"Method": "Method",
"Method - Tooltip": "Method - Tooltip",
"Name": "Name",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Parse Metadata successfully",
"Port": "Port",
@@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Edit Role",
"New Role": "New Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
@@ -381,8 +428,10 @@
"Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!",
"Please input your personal name!": "Please input your personal name!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not valid Email!": "The input is not valid Email!",
@@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Is hashed",
"New Syncer": "New Syncer",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Sync interval - Tooltip",
"Table": "Table",
@@ -424,6 +474,7 @@
"Authorization code": "Authorization code",
"Edit Token": "Edit Token",
"Expires in": "Expires in",
"New Token": "New Token",
"Scope": "Scope",
"Token type": "Token type"
},
@@ -462,6 +513,7 @@
"Modify password...": "Modify password...",
"New Email": "New Email",
"New Password": "New Password",
"New User": "New User",
"New phone": "New phone",
"OK": "OK",
"Old Password": "Old Password",
@@ -498,6 +550,7 @@
"Method": "Method",
"Method - Tooltip": "Method - Tooltip",
"Name": "Name",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Value": "Value"

View File

@@ -14,6 +14,9 @@
"Enable signup": "Activer l'inscription",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Fichier téléchargé avec succès",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Mot de passe activé",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML",
@@ -45,6 +48,7 @@
"Edit Cert": "Modifier le certificat",
"Expire in years": "Expire dans les années",
"Expire in years - Tooltip": "Expire dans les années - infobulle",
"New Cert": "New Cert",
"Private key": "Clé privée",
"Private key - Tooltip": "Clé privée - Infobulle",
"Private key copied to clipboard successfully": "Clé privée copiée dans le presse-papiers avec succès",
@@ -91,6 +95,7 @@
"Avatar": "Avatars",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Retour à la page d'accueil",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@@ -109,6 +114,7 @@
"Email": "Courriel",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Oublier l'URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Domicile",
@@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Est activé - infobulle",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Infobulle",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Mot de passe maître",
"Master password - Tooltip": "Mot de passe maître - Infobulle",
@@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Permissions",
"Personal name": "Nom personnel",
"Phone": "Téléphone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Préfixe du téléphone",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Aperçu",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Fournisseur",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Fournisseurs",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Nom personnel",
"Records": "Enregistrements",
"Request URI": "Request URI",
"Resources": "Ressource",
@@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Synchronisateurs",
"Timestamp": "Horodatage",
"Tokens": "Jetons",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Monter",
"User": "Utilisateur",
"User - Tooltip": "Utilisateur - infobulle",
@@ -233,6 +244,7 @@
"Default avatar": "Avatar par défaut",
"Edit Organization": "Modifier l'organisation",
"Favicon": "Favicon",
"New Organization": "New Organization",
"Soft deletion": "Suppression du logiciel",
"Soft deletion - Tooltip": "Suppression de soft - infobulle",
"Website URL": "URL du site web",
@@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Actions",
@@ -253,10 +266,42 @@
"Edit Permission": "Autorisation d'édition",
"Effect": "Effet",
"Effect - Tooltip": "Effet - Infobulle",
"New Permission": "New Permission",
"Resource type": "Type de ressource",
"Resource type - Tooltip": "Type de ressource - infobulle",
"Resources": "Ressource"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Clé d'accès",
"Access key - Tooltip": "Touche d'accès - Infobulle",
@@ -299,6 +344,7 @@
"Method": "Méthode",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Nom",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Analyse des métadonnées réussie",
"Port": "Port",
@@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Modifier le rôle",
"New Role": "New Role",
"Sub roles": "Sous-rôles",
"Sub roles - Tooltip": "Sous-rôles - infobulle",
"Sub users": "Sous-utilisateurs",
@@ -381,8 +428,10 @@
"Please input your address!": "Veuillez saisir votre adresse !",
"Please input your affiliation!": "Veuillez entrer votre affiliation !",
"Please input your display name!": "Veuillez entrer votre nom d'affichage !",
"Please input your personal name!": "Veuillez entrer votre nom personnel !",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Veuillez entrer votre numéro de téléphone!",
"Please input your real name!": "Veuillez entrer votre nom personnel !",
"Please select your country/region!": "Veuillez sélectionner votre pays/région!",
"Terms of Use": "Conditions d'utilisation",
"The input is not valid Email!": "L'entrée n'est pas un email valide !",
@@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Est haché",
"New Syncer": "New Syncer",
"Sync interval": "Intervalle de synchronisation",
"Sync interval - Tooltip": "Intervalle de synchronisation - infobulle",
"Table": "Tableau",
@@ -424,6 +474,7 @@
"Authorization code": "Code d'autorisation",
"Edit Token": "Modifier le jeton",
"Expires in": "Expire dans",
"New Token": "New Token",
"Scope": "Périmètre d'application",
"Token type": "Type de jeton"
},
@@ -462,6 +513,7 @@
"Modify password...": "Modifier le mot de passe...",
"New Email": "Nouvel e-mail",
"New Password": "Nouveau mot de passe",
"New User": "New User",
"New phone": "Nouveau téléphone",
"OK": "Ok",
"Old Password": "Ancien mot de passe",
@@ -498,6 +550,7 @@
"Method": "Méthode",
"Method - Tooltip": "Méthode - Infobulle",
"Name": "Nom",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Info-bulle",
"Value": "Valeur"

View File

@@ -14,6 +14,9 @@
"Enable signup": "サインアップを有効にする",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "ファイルが正常にアップロードされました",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "パスワードON",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "HTMLファイルを選択してください",
@@ -45,6 +48,7 @@
"Edit Cert": "Certを編集",
"Expire in years": "有効期限",
"Expire in years - Tooltip": "年間有効期限 - ツールチップ",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "秘密鍵を正常にクリップボードにコピーしました",
@@ -91,6 +95,7 @@
"Avatar": "アバター",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "ホーム",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@@ -109,6 +114,7 @@
"Email": "Eメールアドレス",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URLを忘れた",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "ホーム",
@@ -119,6 +125,7 @@
"Is enabled - Tooltip": "有効にする - ツールチップ",
"LDAPs": "LDAP",
"LDAPs - Tooltip": "LDAP - ツールチップ",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "マスターパスワード",
"Master password - Tooltip": "マスターパスワード - ツールチップ",
@@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "アクセス許可",
"Personal name": "個人名",
"Phone": "電話番号",
"Phone - Tooltip": "Phone",
"Phone prefix": "電話プレフィクス",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "プレビュー",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "プロバイダー",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "プロバイダー",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "個人名",
"Records": "レコード",
"Request URI": "Request URI",
"Resources": "リソース",
@@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "タイムスタンプ",
"Tokens": "トークン",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "上へ",
"User": "ユーザー",
"User - Tooltip": "ユーザー → ツールチップ",
@@ -233,6 +244,7 @@
"Default avatar": "デフォルトのアバター",
"Edit Organization": "組織を編集",
"Favicon": "ファビコン",
"New Organization": "New Organization",
"Soft deletion": "ソフト削除",
"Soft deletion - Tooltip": "ソフト削除 - ツールチップ",
"Website URL": "Website URL",
@@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "アクション",
@@ -253,10 +266,42 @@
"Edit Permission": "権限を編集",
"Effect": "効果",
"Effect - Tooltip": "エフェクト - ツールチップ",
"New Permission": "New Permission",
"Resource type": "リソースタイプ",
"Resource type - Tooltip": "リソースタイプ - ツールチップ",
"Resources": "リソース"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "アクセスキー",
"Access key - Tooltip": "アクセスキー → ツールチップ",
@@ -299,6 +344,7 @@
"Method": "方法",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "名前",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "メタデータの解析に成功",
"Port": "ポート",
@@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "役割を編集",
"New Role": "New Role",
"Sub roles": "サブロール",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "サブユーザー",
@@ -381,8 +428,10 @@
"Please input your address!": "住所を入力してください!",
"Please input your affiliation!": "所属を入力してください!",
"Please input your display name!": "表示名を入力してください。",
"Please input your personal name!": "個人名を入力してください!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "電話番号を入力してください!",
"Please input your real name!": "個人名を入力してください!",
"Please select your country/region!": "あなたの国/地域を選択してください!",
"Terms of Use": "利用規約",
"The input is not valid Email!": "入力されたメールアドレスが無効です!",
@@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "ハッシュされました",
"New Syncer": "New Syncer",
"Sync interval": "同期間隔",
"Sync interval - Tooltip": "同期間隔 - ツールチップ",
"Table": "表",
@@ -424,6 +474,7 @@
"Authorization code": "認証コード",
"Edit Token": "トークンを編集",
"Expires in": "有効期限:",
"New Token": "New Token",
"Scope": "スコープ",
"Token type": "トークンの種類"
},
@@ -462,6 +513,7 @@
"Modify password...": "パスワードを変更...",
"New Email": "新しいメール",
"New Password": "新しいパスワード",
"New User": "New User",
"New phone": "新しい電話番号",
"OK": "OK",
"Old Password": "古いパスワード",
@@ -498,6 +550,7 @@
"Method": "方法",
"Method - Tooltip": "方法 - ツールチップ",
"Name": "名前",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL → ツールチップ",
"Value": "値"

View File

@@ -14,6 +14,9 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "File uploaded successfully",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Password ON",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Please select a HTML file",
@@ -45,6 +48,7 @@
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
@@ -91,6 +95,7 @@
"Avatar": "Avatar",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Back Home",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@@ -109,6 +114,7 @@
"Email": "Email",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Forget URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Home",
@@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Is enabled - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip",
@@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Permissions",
"Personal name": "Personal name",
"Phone": "Phone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Preview",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Provider",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Providers",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Real name",
"Records": "Records",
"Request URI": "Request URI",
"Resources": "Resources",
@@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Up",
"User": "User",
"User - Tooltip": "User - Tooltip",
@@ -233,6 +244,7 @@
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
"New Organization": "New Organization",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
"Website URL": "Website URL",
@@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Actions",
@@ -253,10 +266,42 @@
"Edit Permission": "Edit Permission",
"Effect": "Effect",
"Effect - Tooltip": "Effect - Tooltip",
"New Permission": "New Permission",
"Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
@@ -299,6 +344,7 @@
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Name",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Parse Metadata successfully",
"Port": "Port",
@@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Edit Role",
"New Role": "New Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
@@ -381,8 +428,10 @@
"Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!",
"Please input your personal name!": "Please input your personal name!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not valid Email!": "The input is not valid Email!",
@@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Is hashed",
"New Syncer": "New Syncer",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Sync interval - Tooltip",
"Table": "Table",
@@ -424,6 +474,7 @@
"Authorization code": "Authorization code",
"Edit Token": "Edit Token",
"Expires in": "Expires in",
"New Token": "New Token",
"Scope": "Scope",
"Token type": "Token type"
},
@@ -462,6 +513,7 @@
"Modify password...": "Modify password...",
"New Email": "New Email",
"New Password": "New Password",
"New User": "New User",
"New phone": "New phone",
"OK": "OK",
"Old Password": "Old Password",
@@ -498,6 +550,7 @@
"Method": "Method",
"Method - Tooltip": "Method - Tooltip",
"Name": "Name",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Value": "Value"

View File

@@ -14,6 +14,9 @@
"Enable signup": "Включить регистрацию",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Файл успешно загружен",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Пароль ВКЛ",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Пожалуйста, выберите HTML-файл",
@@ -45,6 +48,7 @@
"Edit Cert": "Изменить сертификат",
"Expire in years": "Истекает через годы",
"Expire in years - Tooltip": "Истекает через годы - Подсказка",
"New Cert": "New Cert",
"Private key": "Приватный ключ",
"Private key - Tooltip": "Приватный ключ - Подсказка",
"Private key copied to clipboard successfully": "Приватный ключ скопирован в буфер обмена",
@@ -91,6 +95,7 @@
"Avatar": "Аватар",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Назад",
"Cancel": "Cancel",
"Captcha": "Капча",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@@ -109,6 +114,7 @@
"Email": "Почта",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Забыть URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Домашний",
@@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Включено - Подсказка",
"LDAPs": "LDAPы",
"LDAPs - Tooltip": "LDAPs - Подсказки",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Мастер-пароль",
"Master password - Tooltip": "Мастер-пароль - Tooltip",
@@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Права доступа",
"Personal name": "Личное имя",
"Phone": "Телефон",
"Phone - Tooltip": "Phone",
"Phone prefix": "Префикс телефона",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Предпросмотр",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Поставщик",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Поставщики",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Личное имя",
"Records": "Отчеты",
"Request URI": "Request URI",
"Resources": "Ресурсы",
@@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Синхронизаторы",
"Timestamp": "Отметка времени",
"Tokens": "Жетоны",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Вверх",
"User": "Пользователь",
"User - Tooltip": "Пользователь - Подсказка",
@@ -233,6 +244,7 @@
"Default avatar": "Аватар по умолчанию",
"Edit Organization": "Изменить организацию",
"Favicon": "Иконка",
"New Organization": "New Organization",
"Soft deletion": "Мягкое удаление",
"Soft deletion - Tooltip": "Мягкое удаление - Подсказка",
"Website URL": "URL сайта",
@@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Действия",
@@ -253,10 +266,42 @@
"Edit Permission": "Изменить права доступа",
"Effect": "Эффект",
"Effect - Tooltip": "Эффект - Подсказка",
"New Permission": "New Permission",
"Resource type": "Тип ресурса",
"Resource type - Tooltip": "Тип ресурса - Подсказка",
"Resources": "Ресурсы"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Ключ доступа",
"Access key - Tooltip": "Ключ доступа - Подсказка",
@@ -299,6 +344,7 @@
"Method": "Метод",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Наименование",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Анализ метаданных успешно завершен",
"Port": "Порт",
@@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Изменить роль",
"New Role": "New Role",
"Sub roles": "Суб роли",
"Sub roles - Tooltip": "Суб роли - Tooltip",
"Sub users": "Субпользователи",
@@ -381,8 +428,10 @@
"Please input your address!": "Пожалуйста, введите ваш адрес!",
"Please input your affiliation!": "Пожалуйста, введите вашу партнерство!",
"Please input your display name!": "Пожалуйста, введите ваше отображаемое имя!",
"Please input your personal name!": "Пожалуйста, введите ваше личное имя!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Пожалуйста, введите ваш номер телефона!",
"Please input your real name!": "Пожалуйста, введите ваше личное имя!",
"Please select your country/region!": "Пожалуйста, выберите вашу страну/регион!",
"Terms of Use": "Условия использования",
"The input is not valid Email!": "Ввод не является допустимым Email!",
@@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Хэшировано",
"New Syncer": "New Syncer",
"Sync interval": "Интервал синхронизации",
"Sync interval - Tooltip": "Интервал синхронизации - Tooltip",
"Table": "Таблица",
@@ -424,6 +474,7 @@
"Authorization code": "Код авторизации",
"Edit Token": "Изменить токен",
"Expires in": "Истекает через",
"New Token": "New Token",
"Scope": "Сфера охвата",
"Token type": "Тип токена"
},
@@ -462,6 +513,7 @@
"Modify password...": "Изменить пароль...",
"New Email": "Новое письмо",
"New Password": "Новый пароль",
"New User": "New User",
"New phone": "Новый телефон",
"OK": "ОК",
"Old Password": "Старый пароль",
@@ -498,6 +550,7 @@
"Method": "Метод",
"Method - Tooltip": "Метод - Подсказка",
"Name": "Наименование",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Подсказка",
"Value": "Значение"

View File

@@ -14,6 +14,9 @@
"Enable signup": "启用注册",
"Enable signup - Tooltip": "是否允许用户注册",
"File uploaded successfully": "文件上传成功",
"Grant types": "Grant types",
"Grant types - Tooltip": "选择允许哪些OAuth协议中的Grant types",
"New Application": "添加应用",
"Password ON": "开启密码",
"Password ON - Tooltip": "是否允许密码登录",
"Please select a HTML file": "请选择一个HTML文件",
@@ -45,6 +48,7 @@
"Edit Cert": "编辑证书",
"Expire in years": "有效期(年)",
"Expire in years - Tooltip": "到期年份-工具提示",
"New Cert": "添加证书",
"Private key": "私钥",
"Private key - Tooltip": "私钥 - 工具提示",
"Private key copied to clipboard successfully": "私钥已成功复制到剪贴板",
@@ -91,6 +95,7 @@
"Avatar": "头像",
"Avatar - Tooltip": "向其他人展示的头像",
"Back Home": "返回到首页",
"Cancel": "取消",
"Captcha": "人机验证码",
"Cert": "证书",
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
@@ -109,6 +114,7 @@
"Email": "电子邮箱",
"Email - Tooltip": "电子邮件:",
"Favicon - Tooltip": "网站的图标",
"First name": "名字",
"Forget URL": "忘记密码URL",
"Forget URL - Tooltip": "忘记密码URL",
"Home": "首页",
@@ -119,6 +125,7 @@
"Is enabled - Tooltip": "是否启用",
"LDAPs": "LDAP",
"LDAPs - Tooltip": "LDAPs",
"Last name": "姓氏",
"Logo - Tooltip": "应用程序向外展示的图标",
"Master password": "万能密码",
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
@@ -137,17 +144,18 @@
"Password type - Tooltip": "密码在数据库中存储的形式",
"Payments": "付款",
"Permissions": "权限",
"Personal name": "姓名",
"Phone": "手机号",
"Phone - Tooltip": "手机号",
"Phone prefix": "手机号前缀",
"Phone prefix - Tooltip": "移动电话号码前缀,用于区分国家或地区",
"Preview": "预览",
"Preview - Tooltip": "预览",
"Products": "商品",
"Provider": "提供商",
"Provider - Tooltip": "第三方登录需要配置的提供方",
"Providers": "提供商",
"Providers - Tooltip": "第三方登录需要配置的提供方",
"Real name": "姓名",
"Records": "日志",
"Request URI": "请求URI",
"Resources": "资源",
@@ -161,11 +169,14 @@
"Signup application": "注册应用",
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
"State": "状态",
"State - Tooltip": "状态",
"Swagger": "API文档",
"Syncers": "同步器",
"Timestamp": "时间戳",
"Tokens": "令牌",
"URL": "链接",
"URL - Tooltip": "URL链接",
"Up": "上移",
"User": "用户",
"User - Tooltip": "用户 - 工具提示",
@@ -233,6 +244,7 @@
"Default avatar": "默认头像",
"Edit Organization": "编辑组织",
"Favicon": "图标",
"New Organization": "添加组织",
"Soft deletion": "软删除",
"Soft deletion - Tooltip": "启用后,删除用户信息时不会在数据库彻底清除,只会标记为已删除状态",
"Website URL": "网页地址",
@@ -245,7 +257,8 @@
"Currency - Tooltip": "如USD美元CNY人民币等",
"Edit Payment": "编辑付款",
"Good": "商品",
"Good - Tooltip": "购买的商品名称"
"Good - Tooltip": "购买的商品名称",
"New Payment": "添加付款"
},
"permission": {
"Actions": "动作",
@@ -253,10 +266,42 @@
"Edit Permission": "编辑权限",
"Effect": "效果",
"Effect - Tooltip": "允许还是拒绝",
"New Permission": "添加权限",
"Resource type": "资源类型",
"Resource type - Tooltip": "授权资源的类型",
"Resources": "资源"
},
"product": {
"Alipay": "支付宝",
"Buy Product": "购买商品",
"CNY": "人民币",
"Currency": "币种",
"Currency - Tooltip": "币种 - 工具提示",
"Detail": "详情",
"Detail - Tooltip": "详情 - 工具提示",
"Edit Product": "编辑商品",
"Image": "图片",
"Image - Tooltip": "图片 - 工具提示",
"New Product": "添加商品",
"Pay": "支付方式",
"Payment providers": "支付提供商",
"Payment providers - Tooltip": "支付提供商 - 工具提示",
"Paypal": "Paypal",
"Price": "价格",
"Price - Tooltip": "价格 - 工具提示",
"Quantity": "库存",
"Quantity - Tooltip": "库存 - 工具提示",
"SKU": "货号",
"Sold": "售出",
"Sold - Tooltip": "售出 - 工具提示",
"Tag": "类别",
"Tag - Tooltip": "类别 - 工具提示",
"Test buy page..": "测试购买页面..",
"There is no payment channel for this product.": "该商品没有付款方式。",
"This product is currently not in sale.": "该商品目前未在售。",
"USD": "美元",
"WeChat Pay": "微信支付"
},
"provider": {
"Access key": "访问密钥",
"Access key - Tooltip": "Access key",
@@ -299,6 +344,7 @@
"Method": "方法",
"Method - Tooltip": "登录行为,二维码或者静默授权登录",
"Name": "名称",
"New Provider": "添加提供商",
"Parse": "Parse",
"Parse Metadata successfully": "解析元数据成功",
"Port": "端口",
@@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "编辑角色",
"New Role": "添加角色",
"Sub roles": "包含角色",
"Sub roles - Tooltip": "当前角色所包含的子角色",
"Sub users": "包含用户",
@@ -381,8 +428,10 @@
"Please input your address!": "请输入您的地址!",
"Please input your affiliation!": "请输入您所在的工作单位!",
"Please input your display name!": "请输入您的显示名称!",
"Please input your personal name!": "请输入您的名!",
"Please input your first name!": "请输入您的名",
"Please input your last name!": "请输入您的姓氏!",
"Please input your phone number!": "请输入您的手机号码!",
"Please input your real name!": "请输入您的姓名!",
"Please select your country/region!": "请选择您的国家/地区",
"Terms of Use": "《用户协议》",
"The input is not valid Email!": "您输入的电子邮箱格式有误!",
@@ -410,6 +459,7 @@
"Error text": "错误信息",
"Error text - Tooltip": "同步器连接数据库时发生的错误",
"Is hashed": "是否参与哈希计算",
"New Syncer": "添加同步器",
"Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为秒",
"Table": "表名",
@@ -424,6 +474,7 @@
"Authorization code": "授权码",
"Edit Token": "编辑令牌",
"Expires in": "有效期",
"New Token": "添加令牌",
"Scope": "范围",
"Token type": "令牌类型"
},
@@ -462,6 +513,7 @@
"Modify password...": "编辑密码...",
"New Email": "新邮箱",
"New Password": "新密码",
"New User": "添加用户",
"New phone": "新手机号",
"OK": "确定",
"Old Password": "旧密码",
@@ -498,6 +550,7 @@
"Method": "方法",
"Method - Tooltip": "HTTP方法",
"Name": "名称",
"New Webhook": "添加Webhook",
"URL": "网址",
"URL - Tooltip": "URL",
"Value": "值"