Compare commits

...

70 Commits

Author SHA1 Message Date
40039e0412 feat: add twilio SMS (#1159) 2022-09-25 17:58:12 +08:00
116420adb2 feat: revert "feat: fix openid address format" (#1158)
This reverts commit a447d64bf2.
2022-09-25 09:58:45 +08:00
07c1e3b836 feat: support simple LDAP server (#1155)
* feat:Support simple ldap server

* fix:fix review problems

* fix:fix review problems
2022-09-24 21:48:29 +08:00
a447d64bf2 fix: openid address format (#1157) 2022-09-24 15:34:11 +08:00
4116b1d305 feat: fix google login flash bug (#1147) 2022-09-23 16:03:09 +08:00
1490044295 fix: add returnUrl for user edit page (#1152)
* feat: add redirect param

Signed-off-by: magicwind <2814461814@qq.com>

* Update UserEditPage.js

Signed-off-by: magicwind <2814461814@qq.com>
Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-09-23 12:01:21 +08:00
79f2af405a fix: check whether to use go proxy in build (#1149) 2022-09-22 22:14:25 +08:00
575a248c41 Add TestGetEmailsForUsers() 2022-09-22 20:51:50 +08:00
7083904634 Improve isValidPersonName() 2022-09-21 21:35:39 +08:00
3d50255060 feat: login background image display (#1145) 2022-09-20 23:06:24 +08:00
e295da774f Improve record list page 2022-09-18 23:11:40 +08:00
a3cee496b4 Add add-record API 2022-09-18 17:35:34 +08:00
084a5c3e6b Show logs to org admin 2022-09-18 16:16:45 +08:00
6670450439 Update CI node-version to 16 2022-09-18 15:52:12 +08:00
e1331f314d Add RequireSignedInUser() 2022-09-18 15:43:49 +08:00
604033aa02 feat: use Casdoor to manage Casbin DB table (#1100)
* feat: use Casdoor to manage Casbin DB table

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

* fix: remove trivial codes

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

* chore: go fmt

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

* feat: support role definition

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

* fix: i18n

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

* fix: i18n

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

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-09-14 22:14:13 +08:00
729c20393c fix: missing providers and org in GetDefaultApplication (#1123)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-09-13 22:54:05 +08:00
a90b27b74a Fix admin UI issues 2022-09-13 21:32:18 +08:00
5707e38912 feat: add batchSize to conf (#1120) 2022-09-13 20:31:22 +08:00
ed959bd8c7 feat: improve login page style (#1119)
Signed-off-by: magicwind <2814461814@qq.com>

Signed-off-by: magicwind <2814461814@qq.com>
2022-09-12 00:01:18 +08:00
b6cdc46023 feat: add defaultApplication for Orgnization (#1111)
* feat: add defaultApplication for Orgnization

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

* fix: remove redundant codes

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

* fix: don't use app-built-in

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

* fix: add query param

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

* Update organization.go

* Update organization.go

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-10 20:41:45 +08:00
c661a57cb2 Support regex in CheckRedirectUriValid() 2022-09-10 13:12:36 +08:00
8456b7f7c4 fix: with error pq: column "DingTalk" of relation "user" does not exist (#1116)
* feat: add dingtalk union_id

* fix: with pg, column Dingtalk of relation user table does not exist.

* Update user_util.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-10 13:08:37 +08:00
e8d2906e3c Fix bug in form CSS 2022-09-10 01:33:44 +08:00
1edb91b3a3 feat: custom login form and background (#1107)
* feat: custom login form and background

Signed-off-by: magicwind <2814461814@qq.com>

* feat: costom login form border

* chore: update i18

* Update ApplicationEditPage.js

* Update LoginPage.js

* Update SignupPage.js

* Update LoginPage.js

* Update ApplicationEditPage.js

Signed-off-by: magicwind <2814461814@qq.com>
Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-09-10 00:56:37 +08:00
94b6eb803d Fix WeChat MP login "state too long" bug 2022-09-09 11:43:54 +08:00
cfce5289ed Rename getStateFromQueryParams() and getQueryParamsFromState() 2022-09-09 02:02:32 +08:00
10f1c37730 Fix 403 bug for /api/login/* APIs 2022-09-09 01:54:05 +08:00
6035b98653 feat: add dingtalk union_id (#1110) 2022-09-08 14:44:06 +08:00
e158b58ffa fix: add hidden signal to support chrome extension to auto-signin (#1109)
* feat: add hiden applicationName(support chrome extension to auto recognize applicationName)

* feat: add hiden applicationName for all page

* fix typo

* delete unuseful code

* remove hidden applicationName from login page

* prevent crash if signupApplication is null

* Update App.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-09-07 17:02:28 +08:00
a399184cfc fix: correct edit URL in model list (#1108)
Co-authored-by: Mario Fischer <mario.fischer@inmanet.de>
2022-09-07 00:54:27 +08:00
2f9f946c87 feat: fix GOPROXY bug by exporting environment variable (#1106) 2022-09-05 23:17:39 +08:00
d8b60f838e fix: fix bugs about 3rd-party login in cas flow (#1096) 2022-09-05 23:02:25 +08:00
7599e2715a feat: add demo mode (#1097)
* feat: add demo mode

* feat: add demo mode

* Update app.conf

* Update authz.go

* Update authz.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-04 21:20:19 +08:00
35676455bc chore(style): add keyword spacing rule (#1098) 2022-09-04 19:40:30 +08:00
8128671c8c Improve email code 2022-09-04 12:15:07 +08:00
ee54dec3b3 feat: add support for mysubmail (#1095)
* feat: add support for mysubmail

* Update email.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-04 12:09:50 +08:00
d278bc9651 Add receiver for Email provider 2022-09-04 11:37:36 +08:00
b23bd0b189 Support SUBMAIL email provider 2022-09-04 11:21:20 +08:00
409be85264 Fix placeholder typo 2022-09-03 18:52:35 +08:00
0395b7e1a9 feat: migrate permission data (#1083)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-03 16:45:58 +08:00
4536fd0636 Use app.conf value in getOriginFromHost() 2022-09-03 15:12:34 +08:00
af9ae7dbb7 feat: buildx failed with: EROR: failed to solve: executor failed running [/bin/sh -c ./build.sh]: exit code: 127 (#1089) 2022-09-02 14:50:27 +08:00
e266696b32 feat: add default permission to built-in group (#1087)
* fix: add default permission

* fix: add default permission

* fix: add default permission
2022-09-02 12:03:13 +08:00
e108d26ec7 fix: recover header logo && add styleint check (#1084)
* fix: fix header logo not show

* feat: update lint-staged

feat: add stylelint
2022-08-31 23:26:58 +08:00
349ce7f1d4 fix: refactor build.sh #1081 (#1082)
* fix: Add default access permission for new built-in group users

* fix: Add default access permission for new built-in group users

* fix: File is not `gofumpt`-ed (gofumpt)

* fix: refactor build.sh #1081

* fix: rollback

* fix: newline

* fix: refactor build.sh rename var #1081
2022-08-31 16:08:10 +08:00
8da50b7893 feat: extend managed accounts for get-account api (#1068)
* feat: add get-extend-account api

* feat: extend managed accounts for get-account api

* fix go-linter err

* Use GetApplicationsByOrganizationName
2022-08-30 00:57:27 +08:00
2394c8e2b4 Make sure newStaticBaseUrl is not empty 2022-08-29 21:27:47 +08:00
c62983d734 Use conf.GetConfigString() 2022-08-29 21:26:00 +08:00
5948782cdd fix: fix eslint error in webstorm (#1073) 2022-08-29 15:23:51 +08:00
674d1619dd fix: fix hot update error #1071 (#1072) 2022-08-29 13:45:31 +08:00
11b8b65ca0 feat: update antd and react to latest (#1069) 2022-08-28 23:14:04 +08:00
411d76798d fix: fix upload file security issue (#1063)
* fix: fix upload file security issue

* fix: fix
2022-08-25 11:34:09 +08:00
7b0b426a76 feat: check model grammar when saving and provide a ACL model as init data (#1062)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-08-24 17:21:05 +08:00
a383af0ebc feat: fix token info not contains roles and permissions (#1060)
* fix: fix token info not contains roles and permissions

feat: remove repeated code for obtaining roles and permissions in user controller

* Update user.go

* Update user.go

* Update token.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-08-24 01:41:26 +08:00
f02875e1b1 fix: enable captcha in the application which is not built-in (#1061) 2022-08-23 23:30:45 +08:00
e2921419b9 Add TestDeployStaticFiles() 2022-08-23 21:17:58 +08:00
42864700ec chore: update badges (#1058) 2022-08-23 13:11:42 +08:00
c1fe547939 fix: not start countdown if SMS fails to send (#1057) 2022-08-22 23:06:41 +08:00
267833d9f9 feat: fix the application edit page bug due to this.props.location.search (#1055) 2022-08-22 15:13:58 +08:00
2d3d1167bb Fix HasPromptPage() for signup items 2022-08-22 11:51:20 +08:00
ef5abdfa8f feat: rollback to fix ci (#1051) 2022-08-22 11:14:51 +08:00
580d43101e fix(i18n): add spanish translations (#1043)
* feat(i18n): add spanish translations

* feat(i18n): add missing translations

* feat(i18n): use new icon flag set

* use document protocol

* fix(i18n): use our static flags
2022-08-22 09:42:28 +08:00
fdf2b880cb feat: click on the app card to log in automatically (#1049) 2022-08-22 01:17:18 +08:00
80a2263b18 fix: fix ci bug: "/go/src/casdoor/.git/refs/heads" not found (#1050) 2022-08-22 01:02:57 +08:00
1f11d22c1c fix: add managed account table for supporting Chrome extension to auto login (#1030)
* feat: add manage accounts table(support chrome extension to auto login)

* fix go lint err

* rename manageAccounts to managedAccounts

* expand up&down buttom column width

* rename ManagedAccountsTable to ManagedAccountTable
2022-08-22 00:25:39 +08:00
b6988286b5 Improve i18n for permission page 2022-08-21 23:17:14 +08:00
64f787fab5 feat: can modify static resource url by app.conf (#1045)
* feat: can modify static resource url by app.conf

Signed-off-by: magicwind <2814461814@qq.com>

* Update static_filter.go

Signed-off-by: magicwind <2814461814@qq.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-08-21 21:40:27 +08:00
39c6bd5850 fix: country/region prompted page not show when signin (#1047) 2022-08-21 17:41:07 +08:00
7312c5ce3c Don't check domain for submitPermissionEdit() 2022-08-21 15:28:19 +08:00
102 changed files with 7588 additions and 5832 deletions

View File

@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.17.0'
node-version: 16
# cache
- uses: c-hive/gha-yarn-cache@v2
with:
@ -89,7 +89,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 12
node-version: 16
- name: Fetch Previous version
id: get-previous-tag

View File

@ -19,7 +19,6 @@ RUN apk add curl
RUN apk add ca-certificates && update-ca-certificates
WORKDIR /
COPY --from=BACK /go/src/casdoor/.git/refs/heads .git/refs/heads
COPY --from=BACK /go/src/casdoor/server ./server
COPY --from=BACK /go/src/casdoor/swagger ./swagger
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf
@ -42,7 +41,6 @@ RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates
WORKDIR /
COPY --from=BACK /go/src/casdoor/.git/refs/heads .git/refs/heads
COPY --from=BACK /go/src/casdoor/server ./server
COPY --from=BACK /go/src/casdoor/swagger ./swagger
COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh

View File

@ -8,7 +8,7 @@
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
</a>
<a href="https://github.com/casdoor/casdoor/actions/workflows/build.yml">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casbin/jcasbin/workflows/build/badge.svg?style=flat-square">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casdoor/casdoor/workflows/Build/badge.svg?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casbin/casdoor.svg">
@ -42,65 +42,48 @@
</a>
</p>
## Online demo
- International: https://door.casdoor.org (read-only)
- Asian mirror: https://door.casdoor.com (read-only)
- Asian mirror: https://demo.casdoor.com (read-write, will restore for every 5 minutes)
## Documentation
- International: https://casdoor.org
- Asian mirror: https://docs.casdoor.cn
## Install
- By source code: https://casdoor.org/docs/basic/server-installation
- By Docker: https://casdoor.org/docs/basic/try-with-docker
## How to connect to Casdoor?
https://casdoor.org/docs/how-to-connect/overview
## Casdoor Public API
- Docs: https://casdoor.org/docs/basic/public-api
- Swagger: https://door.casdoor.com/swagger
## Integrations
https://casdoor.org/docs/integration/apisix
## How to contact?
- Gitter: https://gitter.im/casbin/casdoor
- Forum: https://forum.casbin.com
- Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e
## Contribute
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
### I18n translation
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the ```web/``` directory, please remember to add what you have added to the ```web/src/locales/en/data.json``` file.
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the `web/` directory, please remember to add what you have added to the `web/src/locales/en/data.json` file.
## License

View File

@ -15,6 +15,8 @@
package authz
import (
"strings"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
xormadapter "github.com/casbin/xorm-adapter/v2"
@ -28,7 +30,7 @@ func InitAuthz() {
var err error
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
a, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), "casbin_rule", tableNamePrefix, true)
a, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), "casbin_rule", tableNamePrefix, true)
if err != nil {
panic(err)
}
@ -87,6 +89,7 @@ p, *, *, GET, /api/get-organization-applications, *, *
p, *, *, GET, /api/get-user, *, *
p, *, *, GET, /api/get-user-application, *, *
p, *, *, GET, /api/get-resources, *, *
p, *, *, GET, /api/get-records, *, *
p, *, *, GET, /api/get-product, *, *
p, *, *, POST, /api/buy-product, *, *
p, *, *, GET, /api/get-payment, *, *
@ -108,6 +111,7 @@ p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *
`
sa := stringadapter.NewAdapter(ruleText)
@ -128,6 +132,12 @@ p, *, *, GET, /api/get-release, *, *
}
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if conf.IsDemoMode() {
if !isAllowedInDemoMode(subOwner, subName, method, urlPath, objOwner, objName) {
return false
}
}
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)
if err != nil {
panic(err)
@ -135,3 +145,22 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
return res
}
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/send-verification-code" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information
if subOwner == objOwner && subName == objName && !(subOwner == "built-in" && subName == "admin") {
return true
}
return false
} else {
return false
}
}
// If method equals GET
return true
}

View File

@ -1,11 +1,11 @@
#!/bin/bash
#try to connect to google to determine whether user need to use proxy
curl www.google.com -o /dev/null --connect-timeout 5 2 > /dev/null
curl www.google.com -o /dev/null --connect-timeout 5 2> /dev/null
if [ $? == 0 ]
then
echo "Successfully connected to Google, no need to use Go proxy"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
else
echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server .
export GOPROXY="https://goproxy.cn,direct"
fi
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .

BIN
casdoor Normal file

Binary file not shown.

View File

@ -16,4 +16,7 @@ verificationCodeTimeout = 10
initScore = 2000
logPostOnly = true
origin =
staticBaseUrl = "https://cdn.casbin.org"
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
ldapServerPort = 389

View File

@ -24,11 +24,32 @@ import (
"github.com/astaxie/beego"
)
func init() {
// this array contains the beego configuration items that may be modified via env
presetConfigItems := []string{"httpport", "appname"}
for _, key := range presetConfigItems {
if value, ok := os.LookupEnv(key); ok {
err := beego.AppConfig.Set(key, value)
if err != nil {
panic(err)
}
}
}
}
func GetConfigString(key string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return beego.AppConfig.String(key)
res := beego.AppConfig.String(key)
if res == "" {
if key == "staticBaseUrl" {
res = "https://cdn.casbin.org"
}
}
return res
}
func GetConfigBool(key string) (bool, error) {
@ -47,17 +68,7 @@ func GetConfigInt64(key string) (int64, error) {
return num, err
}
func init() {
// this array contains the beego configuration items that may be modified via env
presetConfigItems := []string{"httpport", "appname"}
for _, key := range presetConfigItems {
if value, ok := os.LookupEnv(key); ok {
beego.AppConfig.Set(key, value)
}
}
}
func GetBeegoConfDataSourceName() string {
func GetConfigDataSourceName() string {
dataSourceName := GetConfigString("dataSourceName")
runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
@ -72,3 +83,15 @@ func GetBeegoConfDataSourceName() string {
return dataSourceName
}
func IsDemoMode() bool {
return strings.ToLower(GetConfigString("isDemoMode")) == "true"
}
func GetConfigBatchSize() int {
res, err := strconv.Atoi(GetConfigString("batchSize"))
if err != nil {
res = 100
}
return res
}

View File

@ -258,15 +258,14 @@ func (c *ApiController) Logout() {
// @Success 200 {object} controllers.Response The Response object
// @router /get-account [get]
func (c *ApiController) GetAccount() {
userId, ok := c.RequireSignedIn()
user, ok := c.RequireSignedInUser()
if !ok {
return
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
return
managedAccounts := c.Input().Get("managedAccounts")
if managedAccounts == "1" {
user = object.ExtendManagedAccountsWithUser(user)
}
organization := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
@ -289,18 +288,16 @@ func (c *ApiController) GetAccount() {
// @Success 200 {object} object.Userinfo The Response object
// @router /userinfo [get]
func (c *ApiController) GetUserinfo() {
userId, ok := c.RequireSignedIn()
user, ok := c.RequireSignedInUser()
if !ok {
return
}
scope, aud := c.GetSessionOidc()
host := c.Ctx.Request.Host
resp, err := object.GetUserInfo(userId, scope, aud, host)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = resp
userInfo := object.GetUserInfo(user, scope, aud, host)
c.Data["json"] = userInfo
c.ServeJSON()
}

View File

@ -0,0 +1,94 @@
// 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"
)
func (c *ApiController) GetCasbinAdapters() {
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.GetCasbinAdapters(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetCasbinAdapterCount(owner, field, value)))
adapters := object.GetPaginationCasbinAdapters(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(adapters, paginator.Nums())
}
}
func (c *ApiController) GetCasbinAdapter() {
id := c.Input().Get("id")
c.Data["json"] = object.GetCasbinAdapter(id)
c.ServeJSON()
}
func (c *ApiController) UpdateCasbinAdapter() {
id := c.Input().Get("id")
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateCasbinAdapter(id, &casbinAdapter))
c.ServeJSON()
}
func (c *ApiController) AddCasbinAdapter() {
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddCasbinAdapter(&casbinAdapter))
c.ServeJSON()
}
func (c *ApiController) DeleteCasbinAdapter() {
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteCasbinAdapter(&casbinAdapter))
c.ServeJSON()
}
func (c *ApiController) SyncPolicies() {
id := c.Input().Get("id")
adapter := object.GetCasbinAdapter(id)
c.Data["json"] = object.SyncPolicies(adapter)
c.ServeJSON()
}

131
controllers/ldapserver.go Normal file
View File

@ -0,0 +1,131 @@
// 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 (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
"github.com/forestmgy/ldapserver"
"github.com/lor00x/goldap/message"
)
func StartLdapServer() {
server := ldapserver.NewServer()
routes := ldapserver.NewRouteMux()
routes.Bind(handleBind)
routes.Search(handleSearch).Label(" SEARCH****")
server.Handle(routes)
go server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
// When CTRL+C, SIGINT and SIGTERM signal occurs
// Then stop server gracefully
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
close(ch)
server.Stop()
}
func handleBind(w ldapserver.ResponseWriter, m *ldapserver.Message) {
r := m.GetBindRequest()
res := ldapserver.NewBindResponse(ldapserver.LDAPResultSuccess)
if r.AuthenticationChoice() == "simple" {
bindusername, bindorg, err := object.GetNameAndOrgFromDN(string(r.Name()))
if err != "" {
log.Printf("Bind failed ,ErrMsg=%s", err)
res.SetResultCode(ldapserver.LDAPResultInvalidDNSyntax)
res.SetDiagnosticMessage("bind failed ErrMsg: " + err)
w.Write(res)
return
}
bindpassword := string(r.AuthenticationSimple())
binduser, err := object.CheckUserPassword(bindorg, bindusername, bindpassword)
if err != "" {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
res.SetResultCode(ldapserver.LDAPResultInvalidCredentials)
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err)
w.Write(res)
return
}
if bindorg == "built-in" {
m.Client.IsGlobalAdmin, m.Client.IsOrgAdmin = true, true
} else if binduser.IsAdmin {
m.Client.IsOrgAdmin = true
}
m.Client.IsAuthenticated = true
m.Client.UserName = bindusername
m.Client.OrgName = bindorg
} else {
res.SetResultCode(ldapserver.LDAPResultAuthMethodNotSupported)
res.SetDiagnosticMessage("Authentication method not supported,Please use Simple Authentication")
}
w.Write(res)
}
func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) {
res := ldapserver.NewSearchResultDoneResponse(ldapserver.LDAPResultSuccess)
if !m.Client.IsAuthenticated {
res.SetResultCode(ldapserver.LDAPResultUnwillingToPerform)
w.Write(res)
return
}
r := m.GetSearchRequest()
if r.FilterString() == "(objectClass=*)" {
w.Write(res)
return
}
name, org, errCode := object.GetUserNameAndOrgFromBaseDnAndFilter(string(r.BaseObject()), r.FilterString())
if errCode != ldapserver.LDAPResultSuccess {
res.SetResultCode(errCode)
w.Write(res)
return
}
// Handle Stop Signal (server stop / client disconnected / Abandoned request....)
select {
case <-m.Done:
log.Print("Leaving handleSearch...")
return
default:
}
users, errCode := object.GetFilteredUsers(m, name, org)
if errCode != ldapserver.LDAPResultSuccess {
res.SetResultCode(errCode)
w.Write(res)
return
}
for i := 0; i < len(users); i++ {
user := users[i]
dn := fmt.Sprintf("cn=%s,%s", user.DisplayName, string(r.BaseObject()))
e := ldapserver.NewSearchResultEntry(dn)
e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Name))
e.AddAttribute("email", message.AttributeValue(user.Email))
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
// e.AddAttribute("postalAddress", message.AttributeValue(user.Address[0]))
w.Write(e)
}
w.Write(res)
}

View File

@ -29,7 +29,7 @@ type LinkForm struct {
// @router /unlink [post]
// @Tag Login API
func (c *ApiController) Unlink() {
userId, ok := c.RequireSignedIn()
user, ok := c.RequireSignedInUser()
if !ok {
return
}
@ -44,7 +44,6 @@ func (c *ApiController) Unlink() {
// the user will be unlinked from the provider
unlinkedUser := form.User
user := object.GetUser(userId)
if user.Id != unlinkedUser.Id && !user.IsGlobalAdmin {
// if the user is not the same as the one we are unlinking, we need to make sure the user is the global admin.

View File

@ -121,3 +121,23 @@ func (c *ApiController) DeleteOrganization() {
c.Data["json"] = wrapActionResponse(object.DeleteOrganization(&organization))
c.ServeJSON()
}
// GetDefaultApplication ...
// @Title GetDefaultApplication
// @Tag Organization API
// @Description get default application
// @Param id query string true "organization id"
// @Success 200 {object} Response The Response object
// @router /get-default-application [get]
func (c *ApiController) GetDefaultApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id")
application := object.GetMaskedApplication(object.GetDefaultApplication(id), userId)
if application == nil {
c.ResponseError("Please set a default application for this organization")
return
}
c.ResponseOk(application)
}

View File

@ -55,13 +55,12 @@ func (c *ApiController) GetPermissions() {
// @Success 200 {array} object.Permission The Response object
// @router /get-permissions-by-submitter [get]
func (c *ApiController) GetPermissionsBySubmitter() {
userId, ok := c.RequireSignedIn()
user, ok := c.RequireSignedInUser()
if !ok {
return
}
owner, username := util.GetOwnerAndNameFromId(userId)
permissions := object.GetPermissionsBySubmitter(owner, username)
permissions := object.GetPermissionsBySubmitter(user.Owner, user.Name)
c.ResponseOk(permissions, len(permissions))
return
}

View File

@ -15,6 +15,8 @@
package controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
@ -29,6 +31,11 @@ import (
// @Success 200 {object} object.Record The Response object
// @router /get-records [get]
func (c *ApiController) GetRecords() {
organization, ok := c.RequireAdmin()
if !ok {
return
}
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
@ -40,8 +47,9 @@ func (c *ApiController) GetRecords() {
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetRecordCount(field, value)))
records := object.GetPaginationRecords(paginator.Offset(), limit, field, value, sortField, sortOrder)
filterRecord := &object.Record{Organization: organization}
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetRecordCount(field, value, filterRecord)))
records := object.GetPaginationRecords(paginator.Offset(), limit, field, value, sortField, sortOrder, filterRecord)
c.ResponseOk(records, paginator.Nums())
}
}
@ -66,3 +74,22 @@ func (c *ApiController) GetRecordsByFilter() {
c.Data["json"] = object.GetRecordsByField(record)
c.ServeJSON()
}
// AddRecord
// @Title AddRecord
// @Tag Record API
// @Description add a record
// @Param body body object.Record true "The details of the record"
// @Success 200 {object} controllers.Response The Response object
// @router /add-record [post]
func (c *ApiController) AddRecord() {
var record object.Record
err := json.Unmarshal(c.Ctx.Input.RequestBody, &record)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddRecord(&record))
c.ServeJSON()
}

View File

@ -119,12 +119,7 @@ func (c *ApiController) GetUser() {
user = object.GetUser(id)
}
if user != nil {
roles := object.GetRolesByUser(user.GetId())
user.Roles = roles
permissions := object.GetPermissionsByUser(user.GetId())
user.Permissions = permissions
}
object.ExtendUserWithRolesAndPermissions(user)
c.Data["json"] = object.GetMaskedUser(user)
c.ServeJSON()

View File

@ -75,6 +75,34 @@ func (c *ApiController) RequireSignedIn() (string, bool) {
return userId, true
}
// RequireSignedInUser ...
func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
userId, ok := c.RequireSignedIn()
if !ok {
return nil, false
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
return nil, false
}
return user, true
}
// RequireAdmin ...
func (c *ApiController) RequireAdmin() (string, bool) {
user, ok := c.RequireSignedInUser()
if !ok {
return "", false
}
if user.Owner == "built-in" {
return "", true
}
return user.Owner, true
}
func getInitScore() (int, error) {
return strconv.Atoi(conf.GetConfigString("initScore"))
}

View File

@ -148,17 +148,11 @@ func (c *ApiController) SendVerificationCode() {
// @Title ResetEmailOrPhone
// @router /api/reset-email-or-phone [post]
func (c *ApiController) ResetEmailOrPhone() {
userId, ok := c.RequireSignedIn()
user, ok := c.RequireSignedInUser()
if !ok {
return
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
return
}
destType := c.Ctx.Request.Form.Get("type")
dest := c.Ctx.Request.Form.Get("dest")
code := c.Ctx.Request.Form.Get("code")

70
deployment/deploy.go Normal file
View File

@ -0,0 +1,70 @@
// 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 deployment
import (
"fmt"
"os"
"strings"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/storage"
"github.com/casdoor/casdoor/util"
"github.com/casdoor/oss"
)
func deployStaticFiles(provider *object.Provider) {
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint)
if storageProvider == nil {
panic(fmt.Sprintf("the provider type: %s is not supported", provider.Type))
}
uploadFolder(storageProvider, "js")
uploadFolder(storageProvider, "css")
updateHtml(provider.Domain)
}
func uploadFolder(storageProvider oss.StorageInterface, folder string) {
path := fmt.Sprintf("../web/build/static/%s/", folder)
filenames := util.ListFiles(path)
for _, filename := range filenames {
if !strings.HasSuffix(filename, folder) {
continue
}
file, err := os.Open(path + filename)
if err != nil {
panic(err)
}
objectKey := fmt.Sprintf("static/%s/%s", folder, filename)
_, err = storageProvider.Put(objectKey, file)
if err != nil {
panic(err)
}
fmt.Printf("Uploaded [%s] to [%s]\n", path, objectKey)
}
}
func updateHtml(domainPath string) {
htmlPath := "../web/build/index.html"
html := util.ReadStringFromPath(htmlPath)
html = strings.Replace(html, "\"/static/", fmt.Sprintf("\"%s", domainPath), -1)
util.WriteStringToPath(html, htmlPath)
fmt.Printf("Updated HTML to [%s]\n", html)
}

29
deployment/deploy_test.go Normal file
View File

@ -0,0 +1,29 @@
// 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.
//go:build !skipCi
// +build !skipCi
package deployment
import (
"testing"
"github.com/casdoor/casdoor/object"
)
func TestDeployStaticFiles(t *testing.T) {
provider := object.GetProvider("admin/provider_storage_aliyun_oss")
deployStaticFiles(provider)
}

4
go.mod
View File

@ -10,12 +10,13 @@ require (
github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.30.1
github.com/casbin/xorm-adapter/v2 v2.5.1
github.com/casdoor/go-sms-sender v0.3.0
github.com/casdoor/go-sms-sender v0.4.0
github.com/casdoor/goth v1.69.0-FIX2
github.com/casdoor/oss v1.2.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b
github.com/forestmgy/ldapserver v1.0.9
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-pay/gopay v1.5.72
@ -26,6 +27,7 @@ require (
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lestrrat-go/jwx v0.9.0
github.com/lib/pq v1.8.0
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1

23
go.sum
View File

@ -98,8 +98,8 @@ github.com/casbin/casbin/v2 v2.30.1 h1:P5HWadDL7olwUXNdcuKUBk+x75Y2eitFxYTcLNKeK
github.com/casbin/casbin/v2 v2.30.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/xorm-adapter/v2 v2.5.1 h1:BkpIxRHKa0s3bSMx173PpuU7oTs+Zw7XmD0BIta0HGM=
github.com/casbin/xorm-adapter/v2 v2.5.1/go.mod h1:AeH4dBKHC9/zYxzdPVHhPDzF8LYLqjDdb767CWJoV54=
github.com/casdoor/go-sms-sender v0.3.0 h1:c4bWVcKZhO2L3Xu1oy7aeVkCK6HRJkW/b5K1xU9mV60=
github.com/casdoor/go-sms-sender v0.3.0/go.mod h1:fsZsNnALvFIo+HFcE1U/oCQv4ZT42FdglXKMsEm3WSk=
github.com/casdoor/go-sms-sender v0.4.0 h1:gqlSWq0Z6cymOGOqSGfKDi/C1ZmDy+91JdCaUHUN9qo=
github.com/casdoor/go-sms-sender v0.4.0/go.mod h1:kBykbqwgRDXbXdMAIxmZKinVM1WjdqEbej5LAbUbcfI=
github.com/casdoor/goth v1.69.0-FIX2 h1:RgfIMkL9kekylgxHHK2ZY8ASAwOGns2HVlaBwLu7Bcs=
github.com/casdoor/goth v1.69.0-FIX2/go.mod h1:Om55nRo8CkeDkPSNBbzXW4G5uI28ZUkSk5S69dPek3s=
github.com/casdoor/oss v1.2.0 h1:ozLAE+nnNdFQBWbzH8U9spzaO8h8NrB57lBcdyMUUQ8=
@ -137,6 +137,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/forestmgy/ldapserver v1.0.9 h1:eMJKsN7Q/c9u074FVXfpPuTp3vOyH+O7Zpe6KfCCMUc=
github.com/forestmgy/ldapserver v1.0.9/go.mod h1:1RZ8lox1QSY7rmbjdmy+sYQXY4Lp7SpGzpdE3+j3IyM=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -173,6 +175,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
@ -188,8 +192,9 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -300,6 +305,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 h1:wIONC+HMNRqmWBjuMxhatuSzHaljStc4gjDeKycxy0A=
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0=
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro=
@ -400,6 +407,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -417,6 +425,8 @@ github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03O
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/twilio/twilio-go v0.26.0 h1:wFW4oTe3/LKt6bvByP7eio8JsjtaLHjMQKOUEzQry7U=
github.com/twilio/twilio-go v0.26.0/go.mod h1:lz62Hopu4vicpQ056H5TJ0JE4AP0rS3sQ35/ejmgOwE=
github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA=
github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
@ -427,6 +437,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@ -482,6 +493,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -516,6 +528,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
@ -537,6 +550,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -573,7 +587,9 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -639,6 +655,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -121,6 +121,7 @@ func (idp *DingTalkIdProvider) GetToken(code string) (*oauth2.Token, error) {
type DingTalkUserResponse struct {
Nick string `json:"nick"`
OpenId string `json:"openId"`
UnionId string `json:"unionId"`
AvatarUrl string `json:"avatarUrl"`
Email string `json:"email"`
Errmsg string `json:"message"`
@ -162,6 +163,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
Id: dtUserInfo.OpenId,
Username: dtUserInfo.Nick,
DisplayName: dtUserInfo.Nick,
UnionId: dtUserInfo.UnionId,
Email: dtUserInfo.Email,
AvatarUrl: dtUserInfo.AvatarUrl,
}

View File

@ -25,6 +25,7 @@ type UserInfo struct {
Id string
Username string
DisplayName string
UnionId string
Email string
AvatarUrl string
}

View File

@ -23,6 +23,7 @@ import (
_ "github.com/astaxie/beego/session/redis"
"github.com/casdoor/casdoor/authz"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/controllers"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/routers"
@ -45,7 +46,8 @@ func main() {
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
// beego.DelStaticPath("/static")
beego.SetStaticPath("/static", "web/build/static")
// beego.SetStaticPath("/static", "web/build/static")
beego.BConfig.WebConfig.DirectoryIndex = true
beego.SetStaticPath("/swagger", "swagger")
beego.SetStaticPath("/files", "files")
@ -75,5 +77,8 @@ func main() {
port := beego.AppConfig.DefaultInt("httpport", 8000)
// logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false)
go controllers.StartLdapServer()
beego.Run(fmt.Sprintf(":%v", port))
}

View File

@ -43,7 +43,7 @@ func InitConfig() {
}
func InitAdapter(createDatabase bool) {
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName(), conf.GetConfigString("dbName"))
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
if createDatabase {
adapter.CreateDatabase()
}
@ -145,6 +145,11 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(CasbinAdapter))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Provider))
if err != nil {
panic(err)

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"net/url"
"regexp"
"strings"
"github.com/casdoor/casdoor/util"
@ -66,6 +67,9 @@ type Application struct {
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
FormCss string `xorm:"text" json:"formCss"`
FormOffset int `json:"formOffset"`
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
}
func GetApplicationCount(owner, field, value string) int {
@ -319,7 +323,8 @@ func (application *Application) GetId() string {
func CheckRedirectUriValid(application *Application, redirectUri string) bool {
validUri := false
for _, tmpUri := range application.RedirectUris {
if strings.Contains(redirectUri, tmpUri) {
tmpUriRegex := regexp.MustCompile(tmpUri)
if tmpUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, tmpUri) {
validUri = true
break
}
@ -362,3 +367,34 @@ func IsAllowOrigin(origin string) bool {
return allowOrigin
}
func getApplicationMap(organization string) map[string]*Application {
applications := GetApplicationsByOrganizationName("admin", organization)
applicationMap := make(map[string]*Application)
for _, application := range applications {
applicationMap[application.Name] = application
}
return applicationMap
}
func ExtendManagedAccountsWithUser(user *User) *User {
if user.ManagedAccounts == nil || len(user.ManagedAccounts) == 0 {
return user
}
applicationMap := getApplicationMap(user.Owner)
var managedAccounts []ManagedAccount
for _, managedAccount := range user.ManagedAccounts {
application := applicationMap[managedAccount.Application]
if application != nil {
managedAccount.SigninUrl = application.SigninUrl
managedAccounts = append(managedAccounts, managedAccount)
}
}
user.ManagedAccounts = managedAccounts
return user
}

View File

@ -73,6 +73,10 @@ func (application *Application) IsSignupItemRequired(itemName string) bool {
return signupItem.Required
}
func (si *SignupItem) isSignupItemPrompted() bool {
return si.Visible && si.Prompted
}
func (application *Application) GetSignupItemRule(itemName string) string {
signupItem := application.getSignupItem(itemName)
if signupItem == nil {
@ -92,6 +96,16 @@ func (application *Application) getAllPromptedProviderItems() []*ProviderItem {
return res
}
func (application *Application) getAllPromptedSignupItems() []*SignupItem {
res := []*SignupItem{}
for _, signupItem := range application.SignupItems {
if signupItem.isSignupItemPrompted() {
res = append(res, signupItem)
}
}
return res
}
func (application *Application) isAffiliationPrompted() bool {
signupItem := application.getSignupItem("Affiliation")
if signupItem == nil {
@ -107,5 +121,10 @@ func (application *Application) HasPromptPage() bool {
return true
}
signupItems := application.getAllPromptedSignupItems()
if len(signupItems) != 0 {
return true
}
return application.isAffiliationPrompted()
}

217
object/casbin_adapter.go Normal file
View File

@ -0,0 +1,217 @@
// 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"
"strings"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
xormadapter "github.com/casbin/xorm-adapter/v2"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type CasbinAdapter struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Organization string `xorm:"varchar(100)" json:"organization"`
Type string `xorm:"varchar(100)" json:"type"`
Model string `xorm:"varchar(100)" json:"model"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
Database string `xorm:"varchar(100)" json:"database"`
Table string `xorm:"varchar(100)" json:"table"`
IsEnabled bool `json:"isEnabled"`
Adapter *xormadapter.Adapter `xorm:"-" json:"-"`
}
func GetCasbinAdapterCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&CasbinAdapter{})
if err != nil {
panic(err)
}
return int(count)
}
func GetCasbinAdapters(owner string) []*CasbinAdapter {
adapters := []*CasbinAdapter{}
err := adapter.Engine.Where("owner = ?", owner).Find(&adapters)
if err != nil {
panic(err)
}
return adapters
}
func GetPaginationCasbinAdapters(owner string, page, limit int, field, value, sort, order string) []*CasbinAdapter {
session := GetSession(owner, page, limit, field, value, sort, order)
adapters := []*CasbinAdapter{}
err := session.Find(&adapters)
if err != nil {
panic(err)
}
return adapters
}
func getCasbinAdapter(owner, name string) *CasbinAdapter {
if owner == "" || name == "" {
return nil
}
casbinAdapter := CasbinAdapter{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&casbinAdapter)
if err != nil {
panic(err)
}
if existed {
return &casbinAdapter
} else {
return nil
}
}
func GetCasbinAdapter(id string) *CasbinAdapter {
owner, name := util.GetOwnerAndNameFromId(id)
return getCasbinAdapter(owner, name)
}
func UpdateCasbinAdapter(id string, casbinAdapter *CasbinAdapter) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getCasbinAdapter(owner, name) == nil {
return false
}
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
if casbinAdapter.Password == "***" {
session.Omit("password")
}
affected, err := session.Update(casbinAdapter)
if err != nil {
panic(err)
}
return affected != 0
}
func AddCasbinAdapter(casbinAdapter *CasbinAdapter) bool {
affected, err := adapter.Engine.Insert(casbinAdapter)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteCasbinAdapter(casbinAdapter *CasbinAdapter) bool {
affected, err := adapter.Engine.ID(core.PK{casbinAdapter.Owner, casbinAdapter.Name}).Delete(&CasbinAdapter{})
if err != nil {
panic(err)
}
return affected != 0
}
func (casbinAdapter *CasbinAdapter) GetId() string {
return fmt.Sprintf("%s/%s", casbinAdapter.Owner, casbinAdapter.Name)
}
func (casbinAdapter *CasbinAdapter) getTable() string {
if casbinAdapter.DatabaseType == "mssql" {
return fmt.Sprintf("[%s]", casbinAdapter.Table)
} else {
return casbinAdapter.Table
}
}
func safeReturn(policy []string, i int) string {
if len(policy) > i {
return policy[i]
} else {
return ""
}
}
func matrixToCasbinRules(pType string, policies [][]string) []*xormadapter.CasbinRule {
res := []*xormadapter.CasbinRule{}
for _, policy := range policies {
line := xormadapter.CasbinRule{
PType: pType,
V0: safeReturn(policy, 0),
V1: safeReturn(policy, 1),
V2: safeReturn(policy, 2),
V3: safeReturn(policy, 3),
V4: safeReturn(policy, 4),
V5: safeReturn(policy, 5),
}
res = append(res, &line)
}
return res
}
func SyncPolicies(casbinAdapter *CasbinAdapter) []*xormadapter.CasbinRule {
// init Adapter
if casbinAdapter.Adapter == nil {
var dataSourceName string
if casbinAdapter.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port, casbinAdapter.Database)
} else if casbinAdapter.DatabaseType == "postgres" {
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port, casbinAdapter.Database)
} else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port)
}
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
casbinAdapter.Adapter, _ = xormadapter.NewAdapterByEngineWithTableName(NewAdapter(casbinAdapter.DatabaseType, dataSourceName, casbinAdapter.Database).Engine, casbinAdapter.getTable(), "")
}
// init Model
modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model)
m, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
panic(err)
}
// init Enforcer
enforcer, err := casbin.NewEnforcer(m, casbinAdapter.Adapter)
if err != nil {
panic(err)
}
policies := matrixToCasbinRules("p", enforcer.GetPolicy())
if strings.Contains(modelObj.ModelText, "[role_definition]") {
policies = append(policies, matrixToCasbinRules("g", enforcer.GetGroupingPolicy())...)
}
return policies
}

View File

@ -302,6 +302,10 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
}
if isHit {
containsAsterisk := ContainsAsterisk(userId, permission.Users)
if containsAsterisk {
return true, err
}
enforcer := getEnforcer(permission)
allowed, err = enforcer.Enforce(userId, application.Name, "read")
break

View File

@ -16,10 +16,28 @@
package object
import "github.com/go-gomail/gomail"
import (
"crypto/tls"
"github.com/go-gomail/gomail"
)
func getDialer(provider *Provider) *gomail.Dialer {
dialer := &gomail.Dialer{}
if provider.Type == "SUBMAIL" {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.AppId, provider.ClientSecret)
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} else {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
}
dialer.SSL = !provider.DisableSsl
return dialer
}
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
dialer := getDialer(provider)
message := gomail.NewMessage()
message.SetAddressHeader("From", provider.ClientId, sender)
@ -32,8 +50,7 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
// DailSmtpServer Dail Smtp server
func DailSmtpServer(provider *Provider) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
dialer.SSL = !provider.DisableSsl
dialer := getDialer(provider)
sender, err := dialer.Dial()
if err != nil {

View File

@ -19,14 +19,17 @@ import (
"fmt"
"os"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn"
)
func InitDb() {
MigratePermissionRule()
existed := initBuiltInOrganization()
if !existed {
initBuiltInModel()
initBuiltInPermission()
initBuiltInProvider()
initBuiltInUser()
@ -38,8 +41,6 @@ func InitDb() {
initWebAuthn()
}
var staticBaseUrl = beego.AppConfig.String("staticBaseUrl")
func initBuiltInOrganization() bool {
organization := getOrganization("admin", "built-in")
if organization != nil {
@ -52,10 +53,10 @@ func initBuiltInOrganization() bool {
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Organization",
WebsiteUrl: "https://example.com",
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", staticBaseUrl),
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", conf.GetConfigString("staticBaseUrl")),
PasswordType: "plain",
PhonePrefix: "86",
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", staticBaseUrl),
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Tags: []string{},
AccountItems: []*AccountItem{
{Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
@ -84,6 +85,7 @@ func initBuiltInOrganization() bool {
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
},
}
AddOrganization(organization)
@ -104,7 +106,7 @@ func initBuiltInUser() {
Type: "normal-user",
Password: "123",
DisplayName: "Admin",
Avatar: fmt.Sprintf("%s/img/casbin.svg", staticBaseUrl),
Avatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Email: "admin@example.com",
Phone: "12345678910",
Address: []string{},
@ -134,7 +136,7 @@ func initBuiltInApplication() {
Name: "app-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Casdoor",
Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", staticBaseUrl),
Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", conf.GetConfigString("staticBaseUrl")),
HomepageUrl: "https://casdoor.org",
Organization: "built-in",
Cert: "cert-built-in",
@ -155,6 +157,7 @@ func initBuiltInApplication() {
},
RedirectUris: []string{},
ExpireInHours: 168,
FormOffset: 8,
}
AddApplication(application)
}
@ -238,6 +241,33 @@ func initWebAuthn() {
gob.Register(webauthn.SessionData{})
}
func initBuiltInModel() {
model := GetModel("built-in/model-built-in")
if model != nil {
return
}
model = &Model{
Owner: "built-in",
Name: "model-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Model",
IsEnabled: true,
ModelText: `[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act`,
}
AddModel(model)
}
func initBuiltInPermission() {
permission := GetPermission("built-in/permission-built-in")
if permission != nil {
@ -249,9 +279,10 @@ func initBuiltInPermission() {
Name: "permission-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Permission",
Users: []string{"built-in/admin"},
Users: []string{"built-in/*"},
Roles: []string{},
Domains: []string{},
Model: "model-built-in",
ResourceType: "Application",
Resources: []string{"app-built-in"},
Actions: []string{"Read", "Write", "Admin"},

74
object/ldapserver.go Normal file
View File

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

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"github.com/casbin/casbin/v2/model"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -85,13 +86,19 @@ func GetModel(id string) *Model {
return getModel(owner, name)
}
func UpdateModel(id string, model *Model) bool {
func UpdateModel(id string, modelObj *Model) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getModel(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(model)
// check model grammar
_, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
panic(err)
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(modelObj)
if err != nil {
panic(err)
}

View File

@ -43,6 +43,11 @@ type OidcDiscovery struct {
}
func getOriginFromHost(host string) (string, string) {
origin := conf.GetConfigString("origin")
if origin != "" {
return origin, origin
}
protocol := "https://"
if strings.HasPrefix(host, "localhost") {
protocol = "http://"
@ -58,12 +63,6 @@ func getOriginFromHost(host string) (string, string) {
func GetOidcDiscovery(host string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host)
origin := conf.GetConfigString("origin")
if origin != "" {
originFrontend = origin
originBackend = origin
}
// Examples:
// https://login.okta.com/.well-known/openid-configuration
// https://auth0.auth0.com/.well-known/openid-configuration

View File

@ -41,12 +41,13 @@ type Organization struct {
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
AccountItems []*AccountItem `xorm:"varchar(2000)" json:"accountItems"`
AccountItems []*AccountItem `xorm:"varchar(3000)" json:"accountItems"`
}
func GetOrganizationCount(owner, field, value string) int {
@ -216,3 +217,37 @@ func CheckAccountItemModifyRule(accountItem *AccountItem, user *User) (bool, str
}
return true, ""
}
func GetDefaultApplication(id string) *Application {
organization := GetOrganization(id)
if organization == nil {
return nil
}
if organization.DefaultApplication != "" {
return getApplication("admin", organization.DefaultApplication)
}
applications := []*Application{}
err := adapter.Engine.Asc("created_time").Find(&applications, &Application{Organization: organization.Name})
if err != nil {
panic(err)
}
if len(applications) == 0 {
return nil
}
defaultApplication := applications[0]
for _, application := range applications {
if application.EnableSignUp {
defaultApplication = application
break
}
}
extendApplicationWithProviders(defaultApplication)
extendApplicationWithOrg(defaultApplication)
return defaultApplication
}

View File

@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
@ -207,3 +208,44 @@ func GetPermissionsBySubmitter(owner string, submitter string) []*Permission {
return permissions
}
func MigratePermissionRule() {
models := []*Model{}
err := adapter.Engine.Find(&models, &Model{})
if err != nil {
panic(err)
}
isHit := false
for _, model := range models {
if strings.Contains(model.ModelText, "permission") {
// update model table
model.ModelText = strings.Replace(model.ModelText, "permission,", "", -1)
UpdateModel(model.GetId(), model)
isHit = true
}
}
if isHit {
// update permission_rule table
sql := "UPDATE `permission_rule`SET V0 = V1, V1 = V2, V2 = V3, V3 = V4, V4 = V5 WHERE V0 IN (SELECT CONCAT(owner, '/', name) AS permission_id FROM `permission`)"
_, err = adapter.Engine.Exec(sql)
if err != nil {
return
}
}
}
func ContainsAsterisk(userId string, users []string) bool {
containsAsterisk := false
group, _ := util.GetOwnerAndNameFromId(userId)
for _, user := range users {
permissionGroup, permissionUserName := util.GetOwnerAndNameFromId(user)
if permissionGroup == group && permissionUserName == "*" {
containsAsterisk = true
break
}
}
return containsAsterisk
}

View File

@ -29,7 +29,7 @@ func getEnforcer(permission *Permission) *casbin.Enforcer {
tableName = permission.Adapter
}
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), tableName, tableNamePrefix, true)
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), tableName, tableNamePrefix, true)
if err != nil {
panic(err)
}

View File

@ -48,6 +48,7 @@ type Provider struct {
DisableSsl bool `json:"disableSsl"`
Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"varchar(1000)" json:"content"`
Receiver string `xorm:"varchar(100)" json:"receiver"`
RegionId string `xorm:"varchar(100)" json:"regionId"`
SignName string `xorm:"varchar(100)" json:"signName"`

View File

@ -101,9 +101,9 @@ func AddRecord(record *Record) bool {
return affected != 0
}
func GetRecordCount(field, value string) int {
func GetRecordCount(field, value string, filterRecord *Record) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&Record{})
count, err := session.Count(filterRecord)
if err != nil {
panic(err)
}
@ -121,10 +121,10 @@ func GetRecords() []*Record {
return records
}
func GetPaginationRecords(offset, limit int, field, value, sortField, sortOrder string) []*Record {
func GetPaginationRecords(offset, limit int, field, value, sortField, sortOrder string, filterRecord *Record) []*Record {
records := []*Record{}
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&records)
err := session.Find(&records, filterRecord)
if err != nil {
panic(err)
}

View File

@ -28,7 +28,6 @@ import (
"time"
"github.com/RobotsAndPencils/go-saml"
"github.com/astaxie/beego"
"github.com/beevik/etree"
"github.com/golang-jwt/jwt/v4"
dsig "github.com/russellhaering/goxmldsig"
@ -176,16 +175,12 @@ type Attribute struct {
}
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
//_, originBackend := getOriginFromHost(host)
cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
origin := beego.AppConfig.String("origin")
originFrontend, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
d := IdpEntityDescriptor{
XMLName: xml.Name{
Local: "md:EntityDescriptor",

View File

@ -70,10 +70,12 @@ func GenerateSamlLoginUrl(id, relayState string) (string, string, error) {
}
func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvider, error) {
origin := conf.GetConfigString("origin")
certStore := dsig.MemoryX509CertificateStore{
Roots: []*x509.Certificate{},
}
origin := conf.GetConfigString("origin")
certEncodedData := ""
if samlResponse != "" {
certEncodedData = parseSamlResponse(samlResponse, provider.Type)

View File

@ -103,6 +103,11 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
}
func UploadFileSafe(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, string, error) {
// check fullFilePath is there security issue
if strings.Contains(fullFilePath, "..") {
return "", "", fmt.Errorf("the fullFilePath: %s is not allowed", fullFilePath)
}
var fileUrl string
var objectKey string
var err error

View File

@ -287,6 +287,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
if err != nil {
panic(err)
@ -421,6 +422,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
}
}
ExtendUserWithRolesAndPermissions(user)
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return &TokenError{
@ -571,6 +573,7 @@ func GetPasswordToken(application *Application, username string, password string
}
}
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, &TokenError{
@ -640,6 +643,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
// GetTokenByUser
// Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, host string) (*Token, error) {
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, err
@ -726,6 +730,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
AddUser(user)
}
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host)
if err != nil {
return nil, &TokenError{

View File

@ -18,7 +18,6 @@ import (
"fmt"
"time"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/golang-jwt/jwt/v4"
)
@ -67,11 +66,7 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
user.Password = ""
origin := conf.GetConfigString("origin")
_, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
name := util.GenerateId()
jti := fmt.Sprintf("%s/%s", application.Owner, name)

View File

@ -114,6 +114,8 @@ type User struct {
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
}
type Userinfo struct {
@ -128,6 +130,13 @@ type Userinfo struct {
Phone string `json:"phone,omitempty"`
}
type ManagedAccount struct {
Application string `xorm:"varchar(100)" json:"application"`
Username string `xorm:"varchar(100)" json:"username"`
Password string `xorm:"varchar(100)" json:"password"`
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
}
func GetGlobalUserCount(field, value string) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&User{})
@ -334,6 +343,12 @@ func GetMaskedUser(user *User) *User {
if user.Password != "" {
user.Password = "***"
}
if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts {
manageAccount.Password = "***"
}
}
return user
}
@ -378,7 +393,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
columns = []string{
"owner", "display_name", "avatar",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time",
}
}
@ -473,7 +488,7 @@ func AddUsers(users []*User) bool {
}
func AddUsersInBatch(users []*User) bool {
batchSize := 1000
batchSize := conf.GetConfigBatchSize()
if len(users) == 0 {
return false
@ -507,16 +522,8 @@ 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 := conf.GetConfigString("origin")
func GetUserInfo(user *User, scope string, aud string, host string) *Userinfo {
_, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
resp := Userinfo{
Sub: user.Id,
@ -537,7 +544,7 @@ func GetUserInfo(userId string, scope string, aud string, host string) (*Userinf
if strings.Contains(scope, "phone") {
resp.Phone = user.Phone
}
return &resp, nil
return &resp
}
func LinkUserAccount(user *User, field string, value string) bool {
@ -551,3 +558,12 @@ func (user *User) GetId() string {
func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/")
}
func ExtendUserWithRolesAndPermissions(user *User) {
if user == nil {
return
}
user.Roles = GetRolesByUser(user.GetId())
user.Permissions = GetPermissionsByUser(user.GetId())
}

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/casdoor/casdoor/util"
@ -108,3 +109,24 @@ func TestGetUserByField(t *testing.T) {
t.Log("no user found")
}
}
func TestGetEmailsForUsers(t *testing.T) {
InitConfig()
emailMap := map[string]int{}
emails := []string{}
users := GetUsers("built-in")
for _, user := range users {
if user.Email == "" {
continue
}
if _, ok := emailMap[user.Email]; !ok {
emailMap[user.Email] = 1
emails = append(emails, user.Email)
}
}
text := strings.Join(emails, "\n")
println(text)
}

View File

@ -80,7 +80,7 @@ func SetUserField(user *User, field string, value string) bool {
value = user.Password
}
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{field: value})
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{strings.ToLower(field): value})
if err != nil {
panic(err)
}
@ -137,6 +137,12 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
user.Email = userInfo.Email
}
}
if userInfo.UnionId != "" {
propertyName := fmt.Sprintf("oauth_%s_unionId", providerType)
setUserProperty(user, propertyName, userInfo.UnionId)
}
if userInfo.AvatarUrl != "" {
propertyName := fmt.Sprintf("oauth_%s_avatarUrl", providerType)
setUserProperty(user, propertyName, userInfo.AvatarUrl)

View File

@ -19,7 +19,7 @@ import (
"net/url"
"strings"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/conf"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
@ -27,20 +27,17 @@ import (
func GetWebAuthnObject(host string) *webauthn.WebAuthn {
var err error
origin := beego.AppConfig.String("origin")
if origin == "" {
_, origin = getOriginFromHost(host)
}
_, originBackend := getOriginFromHost(host)
localUrl, err := url.Parse(origin)
localUrl, err := url.Parse(originBackend)
if err != nil {
panic("error when parsing origin:" + err.Error())
}
webAuthn, err := webauthn.New(&webauthn.Config{
RPDisplayName: beego.AppConfig.String("appname"), // Display Name for your site
RPDisplayName: conf.GetConfigString("appname"), // Display Name for your site
RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array
RPOrigin: origin, // The origin URL for WebAuthn requests
RPOrigin: originBackend, // The origin URL for WebAuthn requests
// RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
})
if err != nil {

View File

@ -47,15 +47,15 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
sender := organization.DisplayName
title := provider.Title
code := getRandomCode(5)
code := getRandomCode(6)
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := fmt.Sprintf(provider.Content, code)
if err := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
if err := SendEmail(provider, title, content, dest, sender); err != nil {
return err
}
return SendEmail(provider, title, content, dest, sender)
return AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code)
}
func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
@ -63,12 +63,12 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
return errors.New("please set a SMS provider first")
}
code := getRandomCode(5)
if err := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
code := getRandomCode(6)
if err := SendSms(provider, code, dest); err != nil {
return err
}
return SendSms(provider, code, dest)
return AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code)
}
func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordType, dest, code string) error {

View File

@ -60,6 +60,7 @@ func initAPI() {
beego.Router("/api/update-organization", &controllers.ApiController{}, "POST:UpdateOrganization")
beego.Router("/api/add-organization", &controllers.ApiController{}, "POST:AddOrganization")
beego.Router("/api/delete-organization", &controllers.ApiController{}, "POST:DeleteOrganization")
beego.Router("/api/get-default-application", &controllers.ApiController{}, "GET:GetDefaultApplication")
beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")
@ -96,6 +97,13 @@ func initAPI() {
beego.Router("/api/add-model", &controllers.ApiController{}, "POST:AddModel")
beego.Router("/api/delete-model", &controllers.ApiController{}, "POST:DeleteModel")
beego.Router("/api/get-adapters", &controllers.ApiController{}, "GET:GetCasbinAdapters")
beego.Router("/api/get-adapter", &controllers.ApiController{}, "GET:GetCasbinAdapter")
beego.Router("/api/update-adapter", &controllers.ApiController{}, "POST:UpdateCasbinAdapter")
beego.Router("/api/add-adapter", &controllers.ApiController{}, "POST:AddCasbinAdapter")
beego.Router("/api/delete-adapter", &controllers.ApiController{}, "POST:DeleteCasbinAdapter")
beego.Router("/api/sync-policies", &controllers.ApiController{}, "GET:SyncPolicies")
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone")
@ -147,6 +155,7 @@ func initAPI() {
beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")
beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter")
beego.Router("/api/add-record", &controllers.ApiController{}, "POST:AddRecord")
beego.Router("/api/get-webhooks", &controllers.ApiController{}, "GET:GetWebhooks")
beego.Router("/api/get-webhook", &controllers.ApiController{}, "GET:GetWebhook")

View File

@ -16,12 +16,19 @@ package routers
import (
"net/http"
"os"
"strings"
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
)
var (
oldStaticBaseUrl = "https://cdn.casbin.org"
newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
)
func StaticFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
if strings.HasPrefix(urlPath, "/api/") || strings.HasPrefix(urlPath, "/.well-known/") {
@ -38,9 +45,35 @@ func StaticFilter(ctx *context.Context) {
path += urlPath
}
if util.FileExist(path) {
if !util.FileExist(path) {
path = "web/build/index.html"
}
if oldStaticBaseUrl == newStaticBaseUrl {
http.ServeFile(ctx.ResponseWriter, ctx.Request, path)
} else {
http.ServeFile(ctx.ResponseWriter, ctx.Request, "web/build/index.html")
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, oldStaticBaseUrl, newStaticBaseUrl)
}
}
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, old string, new string) {
f, err := os.Open(name)
if err != nil {
panic(err)
}
defer f.Close()
d, err := f.Stat()
if err != nil {
panic(err)
}
oldContent := util.ReadStringFromPath(name)
newContent := strings.ReplaceAll(oldContent, old, new)
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
_, err = w.Write([]byte(newContent))
if err != nil {
panic(err)
}
}

View File

@ -16,6 +16,7 @@ package util
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
@ -43,6 +44,23 @@ func EnsureFileFolderExists(path string) {
}
}
func ListFiles(path string) []string {
res := []string{}
files, err := ioutil.ReadDir(path)
if err != nil {
panic(err)
}
for _, f := range files {
if !f.IsDir() {
res = append(res, f.Name())
}
}
return res
}
func RemoveExt(filename string) string {
return filename[:len(filename)-len(filepath.Ext(filename))]
}

View File

@ -4,12 +4,18 @@
"es6": true,
"node": true
},
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"requireConfigFile": false,
"babelOptions": {
"babelrc": false,
"configFile": false,
"presets": ["@babel/preset-react"]
}
},
"settings": {
@ -91,6 +97,7 @@
"react/jsx-key": "error",
"no-console": "error",
"eqeqeq": "error",
"keyword-spacing": "error",
"react/prop-types": "off",
"react/display-name": "off",

6
web/.stylelintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recommended-less"
]
}

17
web/babel.config.json Normal file
View File

@ -0,0 +1,17 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}

View File

@ -3,35 +3,35 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@craco/craco": "^6.1.1",
"@crowdin/cli": "^3.6.4",
"@ant-design/icons": "^4.7.0",
"@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"antd": "^4.15.5",
"antd": "^4.22.8",
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.21.1",
"craco-less": "^1.17.1",
"core-js": "^3.25.0",
"craco-less": "^2.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9",
"moment": "^2.29.1",
"qs": "^6.10.2",
"react": "^17.0.2",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1",
"react-cropper": "^2.1.7",
"react-device-detect": "^1.14.0",
"react-dom": "^17.0.2",
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
"react-github-corner": "^2.5.0",
"react-helmet": "^6.1.0",
"react-highlight-words": "^0.17.0",
"react-highlight-words": "^0.18.0",
"react-i18next": "^11.8.7",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.1",
"react-social-login-buttons": "^3.4.0"
},
"scripts": {
@ -41,7 +41,8 @@
"eject": "craco eject",
"crowdin:sync": "crowdin upload && crowdin download",
"preinstall": "node -e \"if (process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Use yarn for installing: https://yarnpkg.com/en/docs/install')\"",
"fix": "eslint --fix ."
"fix": "eslint --fix src/**/*.{js,jsx,ts,tsx}",
"lint:css": "stylelint src/**/*.{css,less} --fix"
},
"eslintConfig": {
"extends": "react-app"
@ -61,15 +62,24 @@
]
},
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"cross-env": "^7.0.3",
"eslint": "^7.11.0",
"eslint-plugin-react": "^7.30.1",
"eslint": "8.22.0",
"eslint-plugin-react": "^7.31.1",
"husky": "^4.3.8",
"lint-staged": "^13.0.3"
"lint-staged": "^13.0.3",
"stylelint": "^14.11.0",
"stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^28.0.0"
},
"lint-staged": {
"src/**/*.{js,jsx,css,sass,ts,tsx}": [
"yarn fix"
"src/**/*.{css,less}": [
"stylelint --fix"
],
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
},
"husky": {

View File

@ -19,7 +19,7 @@
name="description"
content="Casdoor - An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS"
/>
<link rel="apple-touch-icon" href="https://cdn.casdoor.com/static/favicon.png" />
<link rel="apple-touch-icon" href="https://cdn.casbin.org/img/favicon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View File

@ -95,6 +95,7 @@ class AccountTable extends React.Component {
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
{name: "Managed accounts", displayName: i18next.t("user:Managed accounts")},
];
const getItemDisplayName = (text) => {

412
web/src/AdapterEditPage.js Normal file
View File

@ -0,0 +1,412 @@
// 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, Switch, Table, Tooltip} from "antd";
import * as AdapterBackend from "./backend/AdapterBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import "codemirror/lib/codemirror.css";
import * as ModelBackend from "./backend/ModelBackend";
import {EditOutlined, MinusOutlined} from "@ant-design/icons";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/javascript/javascript");
const {Option} = Select;
class AdapterEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
adapterName: props.match.params.adapterName,
adapter: null,
organizations: [],
models: [],
policyLists: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getAdapter();
this.getOrganizations();
}
getAdapter() {
AdapterBackend.getAdapter(this.state.organizationName, this.state.adapterName)
.then((adapter) => {
this.setState({
adapter: adapter,
});
this.getModels(adapter.owner);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations(this.state.organizationName)
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getModels(organizationName) {
ModelBackend.getModels(organizationName)
.then((res) => {
this.setState({
models: res,
});
});
}
parseAdapterField(key, value) {
if (["port"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateAdapterField(key, value) {
value = this.parseAdapterField(key, value);
const adapter = this.state.adapter;
adapter[key] = value;
this.setState({
adapter: adapter,
});
}
synPolicies() {
this.setState({loading: true});
AdapterBackend.syncPolicies(this.state.adapter.owner, this.state.adapter.name)
.then((res) => {
this.setState({loading: false, policyLists: res});
})
.catch(error => {
this.setState({loading: false});
Setting.showMessage("error", `Adapter failed to get policies: ${error}`);
});
}
renderTable(table) {
const columns = [
{
title: "Rule Type",
dataIndex: "PType",
key: "PType",
width: "100px",
},
{
title: "V0",
dataIndex: "V0",
key: "V0",
width: "100px",
},
{
title: "V1",
dataIndex: "V1",
key: "V1",
width: "100px",
},
{
title: "V2",
dataIndex: "V2",
key: "V2",
width: "100px",
},
{
title: "V3",
dataIndex: "V3",
key: "V3",
width: "100px",
},
{
title: "V4",
dataIndex: "V4",
key: "V4",
width: "100px",
},
{
title: "V5",
dataIndex: "V5",
key: "V5",
width: "100px",
},
{
title: "Option",
key: "option",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="topLeft" title="Edit">
<Button style={{marginRight: "0.5rem"}} icon={<EditOutlined />} size="small" />
</Tooltip>
<Tooltip placement="topLeft" title="Delete">
<Button icon={<MinusOutlined />} size="small" />
</Tooltip>
</div>
);
},
}];
return (
<div>
<Table
pagination={{
defaultPageSize: 10,
}}
columns={columns} dataSource={table} rowKey="name" size="middle" bordered
loading={this.state.loading}
/>
</div>
);
}
renderAdapter() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("adapter:New Adapter") : i18next.t("adapter:Edit Adapter")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitAdapterEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitAdapterEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteAdapter()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.organization} onChange={(value => {this.updateadapterField("organization", value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</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.adapter.name} onChange={e => {
this.updateAdapterField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.type} onChange={(value => {
this.updateAdapterField("type", value);
const adapter = this.state.adapter;
// adapter["tableColumns"] = Setting.getAdapterTableColumns(this.state.adapter);
this.setState({
adapter: adapter,
});
})}>
{
["Database"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.host} onChange={e => {
this.updateAdapterField("host", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.adapter.port} onChange={value => {
this.updateAdapterField("port", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.user} onChange={e => {
this.updateAdapterField("user", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.password} onChange={e => {
this.updateAdapterField("password", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.databaseType} onChange={(value => {this.updateAdapterField("databaseType", value);})}>
{
[
{id: "mysql", name: "MySQL"},
{id: "postgres", name: "PostgreSQL"},
{id: "mssql", name: "SQL Server"},
{id: "oracle", name: "Oracle"},
{id: "sqlite3", name: "Sqlite 3"},
].map((databaseType, index) => <Option key={index} value={databaseType.id}>{databaseType.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.database} onChange={e => {
this.updateAdapterField("database", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.table}
disabled={this.state.adapter.type === "Keycloak"} onChange={e => {
this.updateAdapterField("table", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Model"), i18next.t("general:Model - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.model} onChange={(model => {
this.updateAdapterField("model", model);
})}>
{
this.state.models.map((model, index) => <Option key={index} value={model.name}>{model.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("adapter:Policies"), i18next.t("adapter:Policies - Tooltip"))} :
</Col>
<Col span={2}>
<Button type="primary" onClick={() => {this.synPolicies();}}>
{i18next.t("adapter:Sync")}
</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col>
<Col span={22} >
{
this.renderTable(this.state.policyLists)
}
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.adapter.isEnabled} onChange={checked => {
this.updateAdapterField("isEnabled", checked);
}} />
</Col>
</Row>
</Card>
);
}
submitAdapterEdit(willExist) {
const adapter = Setting.deepCopy(this.state.adapter);
AdapterBackend.updateAdapter(this.state.adapter.owner, this.state.adapterName, adapter)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
this.setState({
adapterName: this.state.adapter.name,
});
if (willExist) {
this.props.history.push("/adapters");
} else {
this.props.history.push(`/adapters/${this.state.adapter.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateAdapterField("name", this.state.adapterName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
deleteAdapter() {
AdapterBackend.deleteAdapter(this.state.adapter)
.then(() => {
this.props.history.push("/adapters");
})
.catch(error => {
Setting.showMessage("error", `adapter failed to delete: ${error}`);
});
}
render() {
return (
<div>
{
this.state.adapter !== null ? this.renderAdapter() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitAdapterEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitAdapterEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteAdapter()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default AdapterEditPage;

261
web/src/AdapterListPage.js Normal file
View File

@ -0,0 +1,261 @@
// 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, Popconfirm, Switch, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as AdapterBackend from "./backend/AdapterBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class AdapterListPage extends BaseListPage {
newAdapter() {
const randomName = Setting.getRandomName();
return {
owner: "built-in",
name: `adapter_${randomName}`,
createdTime: moment().format(),
organization: "built-in",
type: "Database",
host: "localhost",
port: 3306,
user: "root",
password: "123456",
databaseType: "mysql",
database: "dbName",
table: "tableName",
isEnabled: false,
};
}
addAdapter() {
const newAdapter = this.newAdapter();
AdapterBackend.addAdapter(newAdapter)
.then((res) => {
this.props.history.push({pathname: `/adapters/${newAdapter.owner}/${newAdapter.name}`, mode: "add"});
}
)
.catch(error => {
Setting.showMessage("error", `Adapter failed to add: ${error}`);
});
}
deleteAdapter(i) {
AdapterBackend.deleteAdapter(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Adapter deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Adapter failed to delete: ${error}`);
});
}
renderTable(adapters) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "150px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/adapters/${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("provider:Type"),
dataIndex: "type",
key: "type",
width: "100px",
sorter: true,
filterMultiple: false,
filters: [
{text: "Database", value: "Database"},
],
},
{
title: i18next.t("provider:Host"),
dataIndex: "host",
key: "host",
width: "120px",
sorter: true,
...this.getColumnSearchProps("host"),
},
{
title: i18next.t("provider:Port"),
dataIndex: "port",
key: "port",
width: "100px",
sorter: true,
...this.getColumnSearchProps("port"),
},
{
title: i18next.t("general:User"),
dataIndex: "user",
key: "user",
width: "120px",
sorter: true,
...this.getColumnSearchProps("user"),
},
{
title: i18next.t("general:Password"),
dataIndex: "password",
key: "password",
width: "120px",
sorter: true,
...this.getColumnSearchProps("password"),
},
{
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "120px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{
title: i18next.t("syncer:Database"),
dataIndex: "database",
key: "database",
width: "120px",
sorter: true,
},
{
title: i18next.t("syncer:Table"),
dataIndex: "table",
key: "table",
width: "120px",
sorter: true,
},
{
title: i18next.t("general:Is enabled"),
dataIndex: "isEnabled",
key: "isEnabled",
width: "120px",
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/adapters/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete adapter: ${record.name} ?`}
onConfirm={() => this.deleteAdapter(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={adapters} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Adapters")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addAdapter.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
AdapterBackend.getAdapters("", 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 AdapterListPage;

View File

@ -72,6 +72,8 @@ import CasLogout from "./auth/CasLogout";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
import SystemInfo from "./SystemInfo";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
const {Header, Footer} = Layout;
@ -123,6 +125,8 @@ class App extends Component {
this.setState({selectedMenuKey: "/permissions"});
} else if (uri.includes("/models")) {
this.setState({selectedMenuKey: "/models"});
} else if (uri.includes("/adapters")) {
this.setState({selectedMenuKey: "/adapters"});
} else if (uri.includes("/providers")) {
this.setState({selectedMenuKey: "/providers"});
} else if (uri.includes("/applications")) {
@ -388,16 +392,15 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/permissions">
<Link to="/permissions">
{i18next.t("general:Permissions")}
</Link>
</Menu.Item>
);
}
res.push(
<Menu.Item key="/permissions">
<Link to="/permissions">
{i18next.t("general:Permissions")}
</Link>
</Menu.Item>
);
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/models">
@ -406,6 +409,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/adapters">
<Link to="/adapters">
{i18next.t("general:Adapters")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
@ -422,7 +432,7 @@ class App extends Component {
);
}
if (Setting.isAdminUser(this.state.account)) {
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/resources">
<Link to="/resources">
@ -430,13 +440,6 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/tokens">
<Link to="/tokens">
{i18next.t("general:Tokens")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/records">
<Link to="/records">
@ -444,6 +447,16 @@ class App extends Component {
</Link>
</Menu.Item>
);
}
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/tokens">
<Link to="/tokens">
{i18next.t("general:Tokens")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/webhooks">
<Link to="/webhooks">
@ -527,7 +540,7 @@ class App extends Component {
}
renderRouter() {
return(
return (
<div>
<Switch>
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
@ -545,6 +558,8 @@ class App extends Component {
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/models" render={(props) => this.renderLoginIfNotLoggedIn(<ModelListPage account={this.state.account} {...props} />)} />
<Route exact path="/models/:organizationName/:modelName" render={(props) => this.renderLoginIfNotLoggedIn(<ModelEditPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
@ -595,7 +610,7 @@ class App extends Component {
// theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px", width: "80%", position: "absolute"}}
style={{lineHeight: "64px", width: "80%", position: "absolute", left: "145px"}}
>
{
this.renderMenu()
@ -618,7 +633,7 @@ class App extends Component {
</div>
);
} else {
return(
return (
<div>
<Header style={{padding: "0", marginBottom: "3px"}}>
{
@ -658,15 +673,18 @@ class App extends Component {
// https://www.freecodecamp.org/neyarnws/how-to-keep-your-footer-where-it-belongs-59c6aa05c59c/
return (
<Footer id="footer" style={
{
borderTop: "1px solid #e8e8e8",
backgroundColor: "white",
textAlign: "center",
}
}>
Made with <span style={{color: "rgb(255, 255, 255)"}}></span> by <a style={{fontWeight: "bold", color: "black"}} target="_blank" href="https://casdoor.org" rel="noreferrer">Casdoor</a>
</Footer>
<>
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
<Footer id="footer" style={
{
borderTop: "1px solid #e8e8e8",
backgroundColor: "white",
textAlign: "center",
}
}>
Made with <span style={{color: "rgb(255, 255, 255)"}}></span> by <a style={{fontWeight: "bold", color: "black"}} target="_blank" href="https://casdoor.org" rel="noreferrer">Casdoor</a>
</Footer>
</>
);
}
@ -682,8 +700,8 @@ class App extends Component {
renderPage() {
if (this.isDoorPages()) {
return (
<div style={{position: "relative", minHeight: "100vh"}}>
<div id="content-wrap" style={{flexDirection: "column"}}>
<div style={{display: "flex", flexDirection: "column", height: "100%"}}>
<div id="login-content-wrap" style={{flexDirection: "column"}}>
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />)} />

View File

@ -1,6 +1,8 @@
@import '~antd/dist/antd.less';
/* stylelint-disable at-rule-name-case */
/* stylelint-disable selector-class-pattern */
@import "~antd/dist/antd.less";
@StaticBaseUrl:"https://cdn.casbin.org";
@StaticBaseUrl: "https://cdn.casbin.org";
.App {
text-align: center;
@ -26,22 +28,32 @@
color: #61dafb;
}
#root{
height: 100%;
}
#parent-area {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
}
#content-wrap {
padding-bottom: 70px; /* Footer height */
display: flex;
flex: 1 1 0;
align-items: stretch;
width: 100%;
}
#login-content-wrap {
display: flex;
flex: 1 1 0;
width: 100%;
}
#footer {
position: absolute;
bottom: 0;
width: 100%;
height: 70px; /* Footer height */
@ -69,8 +81,15 @@
}
.content-warp-card {
box-shadow: 0 1px 5px 0 rgba(51, 51, 51, 0.14);
margin: 5px 5px 5px 5px;
box-shadow: 0 1px 5px 0 rgb(51 51 51 / 14%);
margin: 5px;
flex: 1;
align-items: stretch;
}
.loginBackground {
height: 100%;
background: #ffffff no-repeat;
background-size: 100% 100%;
background-attachment: fixed;
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from "antd";
import {Button, Card, Col, Input, Popover, Radio, Row, Select, Switch, Upload} from "antd";
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend";
@ -35,9 +35,18 @@ import "codemirror/lib/codemirror.css";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml");
require("codemirror/mode/css/css");
const {Option} = Select;
const template = {
padding: "30px",
border: "2px solid #ffffff",
borderRadius: "7px",
backgroundColor: "#ffffff",
boxShadow: " 0px 0px 20px rgba(0, 0, 0, 0.20)",
};
class ApplicationEditPage extends React.Component {
constructor(props) {
super(props);
@ -111,7 +120,7 @@ class ApplicationEditPage extends React.Component {
}
parseApplicationField(key, value) {
if (["expireInHours", "refreshExpireInHours"].includes(key)) {
if (["expireInHours", "refreshExpireInHours", "offset"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
@ -148,6 +157,7 @@ class ApplicationEditPage extends React.Component {
}
renderApplication() {
const preview = JSON.stringify(template, null, 2);
return (
<Card size="small" title={
<div>
@ -536,6 +546,66 @@ class ApplicationEditPage extends React.Component {
this.renderSignupSigninPreview()
}
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Background URL"), i18next.t("application:Background URL - 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.application.formBackgroundUrl} onChange={e => {
this.updateApplicationField("formBackgroundUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Preview")}:
</Col>
<Col span={22} >
<a target="_blank" rel="noreferrer" href={this.state.application.formBackgroundUrl}>
<img src={this.state.application.formBackgroundUrl} alt={this.state.application.formBackgroundUrl} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
</Col>
</Row>
<Row>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Form CSS"), i18next.t("application:Form CSS - Tooltip"))} :
</Col>
<Col span={22}>
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={this.state.application.formCss === "" ? preview : this.state.application.formCss}
options={{mode: "css", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateApplicationField("formCss", value);
}}
/>
</div>
} title={i18next.t("application:Form CSS - Edit")} trigger="click">
<Input value={this.state.application.formCss} style={{marginBottom: "10px"}} onChange={e => {
this.updateApplicationField("formCss", e.target.value);
}} />
</Popover>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:From position"), i18next.t("application:From position - Tooltip"))} :
</Col>
<Col span={22} >
<Radio.Group onChange={e => {this.updateApplicationField("formOffset", e.target.value);}} value={this.state.application.formOffset !== 0 ? this.state.application.formOffset : 8}>
<Radio.Button value={2}>left</Radio.Button>
<Radio.Button value={8}>center</Radio.Button>
<Radio.Button value={14}>right</Radio.Button>
</Radio.Group>
</Col>
</Row>
{
!this.state.application.enableSignUp ? null : (
<Row style={{marginTop: "20px"}} >
@ -591,7 +661,7 @@ class ApplicationEditPage extends React.Component {
<LoginPage type={"login"} mode={"signup"} application={this.state.application} />
)
}
<div style={maskStyle}></div>
<div style={maskStyle} />
</div>
</Col>
<Col span={11}>
@ -605,7 +675,7 @@ class ApplicationEditPage extends React.Component {
<br />
<div style={{position: "relative", 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 style={maskStyle}></div>
<div style={maskStyle} />
</div>
</Col>
</React.Fragment>

View File

@ -53,6 +53,7 @@ class ApplicationListPage extends BaseListPage {
redirectUris: ["http://localhost:9000/callback"],
tokenFormat: "JWT",
expireInHours: 24 * 7,
formOffset: 8,
};
}

View File

@ -39,101 +39,102 @@ class BaseListPage extends React.Component {
this.fetch({pagination});
}
getColumnSearchProps = dataIndex => ({
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
<div style={{padding: 8}}>
<Input
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{marginBottom: 8, display: "block"}}
/>
<Space>
<Button
type="primary"
onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{width: 90}}
>
getColumnSearchProps = dataIndex => ({
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
<div style={{padding: 8}}>
<Input
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{marginBottom: 8, display: "block"}}
/>
<Space>
<Button
type="primary"
onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{width: 90}}
>
Search
</Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
</Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
Reset
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({closeDropdown: false});
this.setState({
searchText: selectedKeys[0],
searchedColumn: dataIndex,
});
}}
>
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({closeDropdown: false});
this.setState({
searchText: selectedKeys[0],
searchedColumn: dataIndex,
});
}}
>
Filter
</Button>
</Space>
</div>
</Button>
</Space>
</div>
),
filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1890ff" : undefined}} />,
onFilter: (value, record) =>
record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: "",
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}
},
render: text =>
this.state.searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
searchWords={[this.state.searchText]}
autoEscape
textToHighlight={text ? text.toString() : ""}
/>
) : (
text
),
filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1890ff" : undefined}} />,
onFilter: (value, record) =>
record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: "",
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}
},
render: text =>
this.state.searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
searchWords={[this.state.searchText]}
autoEscape
textToHighlight={text ? text.toString() : ""}
/>
) : (
text
),
});
handleSearch = (selectedKeys, confirm, dataIndex) => {
this.fetch({searchText: selectedKeys[0], searchedColumn: dataIndex, pagination: this.state.pagination});
};
handleReset = clearFilters => {
clearFilters();
const {pagination} = this.state;
this.fetch({pagination});
};
handleTableChange = (pagination, filters, sorter) => {
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
pagination,
...filters,
searchText: this.state.searchText,
searchedColumn: this.state.searchedColumn,
});
};
handleSearch = (selectedKeys, confirm, dataIndex) => {
this.fetch({searchText: selectedKeys[0], searchedColumn: dataIndex, pagination: this.state.pagination});
};
handleReset = clearFilters => {
clearFilters();
const {pagination} = this.state;
this.fetch({pagination});
};
handleTableChange = (pagination, filters, sorter) => {
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
pagination,
...filters,
searchText: this.state.searchText,
searchedColumn: this.state.searchedColumn,
});
};
render() {
return (
<div>
{
this.renderTable(this.state.data)
}
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.data)
}
</div>
);
}
}
export default BaseListPage;

View File

@ -0,0 +1,160 @@
// 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 {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd";
import * as Setting from "./Setting";
import i18next from "i18next";
const {Option} = Select;
class ManagedAccountTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {application: "", username: "", password: ""};
if (table === undefined || table === null) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
render: (text, record, index) => {
const items = this.props.applications;
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
onChange={value => {
this.updateField(table, index, "application", value);
}} >
{
items.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
}
</Select>
);
},
},
{
title: i18next.t("signup:Username"),
dataIndex: "username",
key: "username",
width: "420px",
render: (text, record, index) => {
return (
<Input defaultValue={text} onChange={e => {
this.updateField(table, index, "username", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Password"),
dataIndex: "password",
key: "password",
width: "420px",
render: (text, record, index) => {
return (
<Input defaultValue={text} onChange={e => {
this.updateField(table, index, "password", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Action"),
key: "action",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table scroll={{x: "max-content"}} rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
);
}
}
export default ManagedAccountTable;

View File

@ -88,7 +88,7 @@ class ModelListPage extends BaseListPage {
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/models/${text}`}>
<Link to={`/models/${record.owner}/${text}`}>
{text}
</Link>
);

View File

@ -15,6 +15,7 @@
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
@ -31,6 +32,7 @@ class OrganizationEditPage extends React.Component {
classes: props,
organizationName: props.match.params.organizationName,
organization: null,
applications: [],
ldaps: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
@ -38,6 +40,7 @@ class OrganizationEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getOrganization();
this.getApplications();
this.getLdaps();
}
@ -50,6 +53,15 @@ class OrganizationEditPage extends React.Component {
});
}
getApplications() {
ApplicationBackend.getApplicationsByOrganization("admin", this.state.organizationName)
.then((applications) => {
this.setState({
applications: applications,
});
});
}
getLdaps() {
LdapBackend.getLdaps(this.state.organizationName)
.then(res => {
@ -209,6 +221,18 @@ class OrganizationEditPage extends React.Component {
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Default application"), i18next.t("general:Default application - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.defaultApplication} onChange={(value => {this.updateOrganizationField("defaultApplication", value);})}>
{
this.state.applications?.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :

View File

@ -35,6 +35,7 @@ class OrganizationListPage extends BaseListPage {
PasswordSalt: "",
phonePrefix: "86",
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",
tags: [],
masterPassword: "",
enableSoftDeletion: false,

View File

@ -245,7 +245,8 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Application", name: "Application"},
{id: "Application", name: i18next.t("general:Application")},
{id: "TreeNode", name: i18next.t("permission:TreeNode")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -273,9 +274,9 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Read", name: "Read"},
{id: "Write", name: "Write"},
{id: "Admin", name: "Admin"},
{id: "Read", name: i18next.t("permission:Read")},
{id: "Write", name: i18next.t("permission:Write")},
{id: "Admin", name: i18next.t("permission:Admin")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -291,8 +292,8 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Allow", name: "Allow"},
{id: "Deny", name: "Deny"},
{id: "Allow", name: i18next.t("permission:Allow")},
{id: "Deny", name: i18next.t("permission:Deny")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -358,8 +359,8 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Approved", name: "Approved"},
{id: "Pending", name: "Pending"},
{id: "Approved", name: i18next.t("permission:Approved")},
{id: "Pending", name: i18next.t("permission:Pending")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -374,10 +375,10 @@ class PermissionEditPage extends React.Component {
Setting.showMessage("error", "The users and roles cannot be empty at the same time");
return;
}
if (this.state.permission.domains.length === 0) {
Setting.showMessage("error", "The domains cannot be empty");
return;
}
// if (this.state.permission.domains.length === 0) {
// Setting.showMessage("error", "The domains cannot be empty");
// return;
// }
if (this.state.permission.resources.length === 0) {
Setting.showMessage("error", "The resources cannot be empty");
return;

View File

@ -188,7 +188,19 @@ class PermissionListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("actions"),
render: (text, record, index) => {
return Setting.getTags(text);
const tags = text.map((tag, i) => {
switch (tag) {
case "Read":
return i18next.t("permission:Read");
case "Write":
return i18next.t("permission:Write");
case "Admin":
return i18next.t("permission:Admin");
default:
return null;
}
});
return Setting.getTags(tags);
},
},
{
@ -197,11 +209,21 @@ class PermissionListPage extends BaseListPage {
key: "effect",
filterMultiple: false,
filters: [
{text: "Allow", value: "Allow"},
{text: "Deny", value: "Deny"},
{text: i18next.t("permission:Allow"), value: "Allow"},
{text: i18next.t("permission:Deny"), value: "Deny"},
],
width: "120px",
sorter: true,
render: (text, record, index) => {
switch (text) {
case "Allow":
return Setting.getTag("success", i18next.t("permission:Allow"));
case "Deny":
return Setting.getTag("error", i18next.t("permission:Deny"));
default:
return null;
}
},
},
{
title: i18next.t("general:Is enabled"),
@ -248,11 +270,21 @@ class PermissionListPage extends BaseListPage {
key: "state",
filterMultiple: false,
filters: [
{text: "Approved", value: "Approved"},
{text: "Pending", value: "Pending"},
{text: i18next.t("permission:Approved"), value: "Approved"},
{text: i18next.t("permission:Pending"), value: "Pending"},
],
width: "120px",
sorter: true,
render: (text, record, index) => {
switch (text) {
case "Approved":
return Setting.getTag("success", i18next.t("permission:Approved"));
case "Pending":
return Setting.getTag("error", i18next.t("permission:Pending"));
default:
return null;
}
},
},
{
title: i18next.t("general:Action"),

View File

@ -34,7 +34,6 @@ class ProviderEditPage extends React.Component {
providerName: props.match.params.providerName,
provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
testEmail: this.props.account["email"] !== undefined ? this.props.account["email"] : "",
};
}
@ -131,6 +130,9 @@ class ProviderEditPage extends React.Component {
} else if (this.state.provider.category === "SMS" && this.state.provider.type === "Huawei Cloud SMS") {
text = i18next.t("provider:Channel No.");
tooltip = i18next.t("provider:Channel No. - Tooltip");
} else if (this.state.provider.category === "Email" && this.state.provider.type === "SUBMAIL") {
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
} else {
return null;
}
@ -199,9 +201,12 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "GitHub");
} else if (value === "Email") {
this.updateProviderField("type", "Default");
this.updateProviderField("host", "smtp.example.com");
this.updateProviderField("port", 465);
this.updateProviderField("disableSsl", false);
this.updateProviderField("title", "Casdoor Verification Code");
this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes.");
this.updateProviderField("receiver", this.props.account.email);
} else if (value === "SMS") {
this.updateProviderField("type", "Aliyun SMS");
} else if (value === "Storage") {
@ -536,7 +541,7 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:Email Content"), i18next.t("provider:Email Content - Tooltip"))} :
</Col>
<Col span={22} >
<TextArea autoSize={{minRows: 1, maxRows: 100}} value={this.state.provider.content} onChange={e => {
<TextArea autoSize={{minRows: 3, maxRows: 100}} value={this.state.provider.content} onChange={e => {
this.updateProviderField("content", e.target.value);
}} />
</Col>
@ -546,35 +551,36 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:Test Email"), i18next.t("provider:Test Email - Tooltip"))} :
</Col>
<Col span={4} >
<Input value={this.state.testEmail}
placeHolder = {i18next.t("user:Input your email")}
onChange={e => {
this.setState({testEmail: e.target.value});
}} />
<Input value={this.state.provider.receiver} placeholder = {i18next.t("user:Input your email")} onChange={e => {
this.updateProviderField("receiver", e.target.value);
}} />
</Col>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test Connection")}
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidEmail(this.state.testEmail)}
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.testEmail)} >
disabled={!Setting.isValidEmail(this.state.provider.receiver)}
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.provider.receiver)} >
{i18next.t("provider:Send Test Email")}
</Button>
</Row>
</React.Fragment>
) : this.state.provider.category === "SMS" ? (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Sign Name"), i18next.t("provider:Sign Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.signName} onChange={e => {
this.updateProviderField("signName", e.target.value);
}} />
</Col>
</Row>
{this.state.provider.type === "Twilio SMS" ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Sign Name"), i18next.t("provider:Sign Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.signName} onChange={e => {
this.updateProviderField("signName", e.target.value);
}} />
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Template Code"), i18next.t("provider:Template Code - Tooltip"))} :
@ -609,7 +615,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={2}></Col>
<Col style={{marginTop: "5px"}} span={2} />
<Col span={2}>
<Button type="primary" onClick={() => {
try {

View File

@ -44,7 +44,7 @@ class RecordListPage extends BaseListPage {
}
renderTable(records) {
const columns = [
let columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
@ -174,6 +174,10 @@ class RecordListPage extends BaseListPage {
},
];
if (Setting.isLocalAdminUser(this.props.account)) {
columns = columns.filter(column => column.key !== "name" && column.key !== "organization");
}
const paginationProps = {
total: this.state.pagination.total,
pageSize: this.state.pagination.pageSize,

View File

@ -56,8 +56,12 @@ export const ResetModal = (props) => {
});
};
let placeHolder = "";
if (destType === "email") {placeHolder = i18next.t("user:Input your email");} else if (destType === "phone") {placeHolder = i18next.t("user:Input your phone number");}
let placeholder = "";
if (destType === "email") {
placeholder = i18next.t("user:Input your email");
} else if (destType === "phone") {
placeholder = i18next.t("user:Input your phone number");
}
return (
<Row>
@ -80,7 +84,7 @@ export const ResetModal = (props) => {
<Input
addonBefore={destType === "email" ? i18next.t("user:New Email") : i18next.t("user:New phone")}
prefix={destType === "email" ? <MailOutlined /> : <PhoneOutlined />}
placeholder={placeHolder}
placeholder={placeholder}
onChange={e => setDest(e.target.value)}
/>
</Row>
@ -89,6 +93,7 @@ export const ResetModal = (props) => {
textBefore={i18next.t("code:Code You Received")}
onChange={setCode}
onButtonClickArgs={[dest, destType, Setting.getApplicationName(application)]}
application={application}
/>
</Row>
</Col>

View File

@ -15,12 +15,13 @@
import React from "react";
import * as Setting from "./Setting";
import {Dropdown, Menu} from "antd";
import {createFromIconfontCN} from "@ant-design/icons";
import "./App.less";
const IconFont = createFromIconfontCN({
scriptUrl: "//at.alicdn.com/t/font_2680620_ffij16fkwdg.js",
});
function flagIcon(country, alt) {
return (
<img width={24} alt={alt} src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} />
);
}
class SelectLanguageBox extends React.Component {
constructor(props) {
@ -35,13 +36,14 @@ class SelectLanguageBox extends React.Component {
<Menu onClick={(e) => {
Setting.changeLanguage(e.key);
}}>
<Menu.Item key="en" icon={<IconFont type="icon-en" />}>English</Menu.Item>
<Menu.Item key="zh" icon={<IconFont type="icon-zh" />}>简体中文</Menu.Item>
<Menu.Item key="fr" icon={<IconFont type="icon-fr" />}>Français</Menu.Item>
<Menu.Item key="de" icon={<IconFont type="icon-de" />}>Deutsch</Menu.Item>
<Menu.Item key="ja" icon={<IconFont type="icon-ja" />}>日本語</Menu.Item>
<Menu.Item key="ko" icon={<IconFont type="icon-ko" />}>한국어</Menu.Item>
<Menu.Item key="ru" icon={<IconFont type="icon-ru" />}>Русский</Menu.Item>
<Menu.Item key="en" icon={flagIcon("US", "English")}>English</Menu.Item>
<Menu.Item key="es" icon={flagIcon("ES", "Español")}>Español</Menu.Item>
<Menu.Item key="zh" icon={flagIcon("CN", "简体中文")}>简体中文</Menu.Item>
<Menu.Item key="fr" icon={flagIcon("FR", "Français")}>Français</Menu.Item>
<Menu.Item key="de" icon={flagIcon("DE", "Deutsch")}>Deutsch</Menu.Item>
<Menu.Item key="ja" icon={flagIcon("JP", "日本語")}>日本語</Menu.Item>
<Menu.Item key="ko" icon={flagIcon("KR", "한국어")}>한국어</Menu.Item>
<Menu.Item key="ru" icon={flagIcon("RU", "Русский")}>Русский</Menu.Item>
</Menu>
);

View File

@ -49,6 +49,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_huawei.png`,
url: "https://www.huaweicloud.com/product/msgsms.html",
},
"Twilio SMS": {
logo: `${StaticBaseUrl}/img/social_twilio.png`,
url: "https://www.twilio.com/messaging",
},
"Mock SMS": {
logo: `${StaticBaseUrl}/img/social_default.png`,
url: "",
@ -82,7 +86,7 @@ export const OtherProviderInfo = {
url: "https://cloud.tencent.com/product/cos",
},
"Azure Blob": {
logo: `${StaticBaseUrl}/img/social_azure.jpg`,
logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://azure.microsoft.com/en-us/services/storage/blobs/",
},
},
@ -229,9 +233,11 @@ export function getSignupItem(application, itemName) {
}
export function isValidPersonName(personName) {
// https://blog.css8.cn/post/14210975.html
const personNameRegex = /^[\u4e00-\u9fa5]{2,6}$/;
return personNameRegex.test(personName);
return personName !== "";
// // https://blog.css8.cn/post/14210975.html
// const personNameRegex = /^[\u4e00-\u9fa5]{2,6}$/;
// return personNameRegex.test(personName);
}
export function isValidIdCard(idCard) {
@ -320,6 +326,19 @@ function isProviderItemAnswered(user, application, providerItem) {
return linkedValue !== undefined && linkedValue !== "";
}
function isSignupItemAnswered(user, signupItem) {
if (user === null) {
return false;
}
if (signupItem.name !== "Country/Region") {
return true;
}
const value = user["region"];
return value !== undefined && value !== "";
}
export function isPromptAnswered(user, application) {
if (!isAffiliationAnswered(user, application)) {
return false;
@ -331,9 +350,24 @@ export function isPromptAnswered(user, application) {
return false;
}
}
const signupItems = getAllPromptedSignupItems(application);
for (let i = 0; i < signupItems.length; i++) {
if (!isSignupItemAnswered(user, signupItems[i])) {
return false;
}
}
return true;
}
export function parseObject(s) {
try {
return eval("(" + s + ")");
} catch (e) {
return null;
}
}
export function parseJson(s) {
if (s === "") {
return null;
@ -430,9 +464,9 @@ export function trim(str, ch) {
let start = 0;
let end = str.length;
while(start < end && str[start] === ch) {++start;}
while (start < end && str[start] === ch) {++start;}
while(end > start && str[end - 1] === ch) {--end;}
while (end > start && str[end - 1] === ch) {--end;}
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
}
@ -480,7 +514,7 @@ export function getFriendlyFileSize(size) {
return `${num} ${"KMGTPEZY"[i - 1]}B`;
}
function getRandomInt(s) {
function getHashInt(s) {
let hash = 0;
if (s.length !== 0) {
for (let i = 0; i < s.length; i++) {
@ -490,16 +524,16 @@ function getRandomInt(s) {
}
}
if (hash < 0) {
hash = -hash;
}
return hash;
}
export function getAvatarColor(s) {
const colorList = ["#f56a00", "#7265e6", "#ffbf00", "#00a2ae"];
let random = getRandomInt(s);
if (random < 0) {
random = -random;
}
return colorList[random % 4];
const hash = getHashInt(s);
return colorList[hash % 4];
}
export function getLanguage() {
@ -613,6 +647,7 @@ export function getProviderTypeOptions(category) {
return (
[
{id: "Default", name: "Default"},
{id: "SUBMAIL", name: "SUBMAIL"},
]
);
} else if (category === "SMS") {
@ -622,6 +657,7 @@ export function getProviderTypeOptions(category) {
{id: "Tencent Cloud SMS", name: "Tencent Cloud SMS"},
{id: "Volc Engine SMS", name: "Volc Engine SMS"},
{id: "Huawei Cloud SMS", name: "Huawei Cloud SMS"},
{id: "Twilio SMS", name: "Twilio SMS"},
]
);
} else if (category === "Storage") {
@ -686,12 +722,12 @@ export function renderLogo(application) {
if (application.homepageUrl !== "") {
return (
<a target="_blank" rel="noreferrer" href={application.homepageUrl}>
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "30px"}} />
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
</a>
);
} else {
return (
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "30px"}} />
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
);
}
}
@ -837,7 +873,10 @@ export function getTagColor(s) {
export function getTags(tags) {
const res = [];
if (!tags) {return res;}
if (!tags) {
return res;
}
tags.forEach((tag, i) => {
res.push(
<Tag color={getTagColor(tag)}>
@ -848,6 +887,14 @@ export function getTags(tags) {
return res;
}
export function getTag(color, text) {
return (
<Tag color={color}>
{text}
</Tag>
);
}
export function getApplicationOrgName(application) {
return `${application?.organizationObj.owner}/${application?.organizationObj.name}`;
}
@ -881,6 +928,14 @@ export function scrollToDiv(divId) {
}
}
export function inIframe() {
try {
return window !== window.parent;
} catch (e) {
return true;
}
}
export function getSyncerTableColumns(syncer) {
switch (syncer.type) {
case "Keycloak":

View File

@ -28,6 +28,7 @@ import OAuthWidget from "./common/OAuthWidget";
import SamlWidget from "./common/SamlWidget";
import SelectRegionBox from "./SelectRegionBox";
import WebAuthnCredentialTable from "./WebauthnCredentialTable";
import ManagedAccountTable from "./ManagedAccountTable";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
@ -100,6 +101,11 @@ class UserEditPage extends React.Component {
});
}
getReturnUrl() {
const searchParams = new URLSearchParams(this.props.location.search);
return searchParams.get("returnUrl");
}
parseUserField(key, value) {
// if ([].includes(key)) {
// value = Setting.myParseInt(value);
@ -498,7 +504,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:Is admin"), i18next.t("user:Is admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isAdmin} onChange={checked => {
<Switch disabled={this.state.user.owner === "built-in"} checked={this.state.user.isAdmin} onChange={checked => {
this.updateUserField("isAdmin", checked);
}} />
</Col>
@ -511,7 +517,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:Is global admin"), i18next.t("user:Is global admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isGlobalAdmin} onChange={checked => {
<Switch disabled={this.state.user.owner === "built-in"} checked={this.state.user.isGlobalAdmin} onChange={checked => {
this.updateUserField("isGlobalAdmin", checked);
}} />
</Col>
@ -543,7 +549,7 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if(accountItem.name === "WebAuthn credentials") {
} else if (accountItem.name === "WebAuthn credentials") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@ -554,6 +560,22 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Managed accounts") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Managed accounts"), i18next.t("user:Managed accounts"))} :
</Col>
<Col span={22} >
<ManagedAccountTable
title={i18next.t("user:Managed accounts")}
table={this.state.user.managedAccounts ?? []}
onUpdateTable={(table) => {this.updateUserField("managedAccounts", table);}}
applications={this.state.applications}
/>
</Col>
</Row>
);
}
}
@ -599,6 +621,13 @@ class UserEditPage extends React.Component {
} else {
this.props.history.push(`/users/${this.state.user.owner}/${this.state.user.name}`);
}
} else {
if (willExist) {
const returnUrl = this.getReturnUrl();
if (returnUrl) {
window.location.href = returnUrl;
}
}
}
} else {
Setting.showMessage("error", res.msg);

View File

@ -41,8 +41,9 @@ class UserListPage extends BaseListPage {
newUser() {
const randomName = Setting.getRandomName();
const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
return {
owner: "built-in", // this.props.account.username,
owner: owner,
name: `user_${randomName}`,
createdTime: moment().format(),
type: "normal-user",
@ -56,8 +57,8 @@ class UserListPage extends BaseListPage {
affiliation: "Example Inc.",
tag: "staff",
region: "",
isAdmin: false,
isGlobalAdmin: false,
isAdmin: (owner === "built-in"),
isGlobalAdmin: (owner === "built-in"),
IsForbidden: false,
isDeleted: false,
properties: {},
@ -326,6 +327,7 @@ class UserListPage extends BaseListPage {
width: "190px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name);
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/users/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
@ -333,7 +335,7 @@ class UserListPage extends BaseListPage {
title={`Sure to delete user: ${record.name} ?`}
onConfirm={() => this.deleteUser(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button disabled={disabled} style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -35,7 +35,7 @@ class AuthCallback extends React.Component {
// realRedirectUrl = "http://localhost:9000"
const params = new URLSearchParams(this.props.location.search);
const state = params.get("state");
const queryString = Util.stateToGetQueryParams(state);
const queryString = Util.getQueryParamsFromState(state);
return new URLSearchParams(queryString);
}
@ -50,8 +50,12 @@ class AuthCallback extends React.Component {
// Casdoor's own login page, so "code" is not necessary
if (realRedirectUri === null) {
const samlRequest = innerParams.get("SAMLRequest");
// cas don't use 'redirect_url', it is called 'service'
const casService = innerParams.get("service");
if (samlRequest !== null && samlRequest !== undefined && samlRequest !== "") {
return "saml";
} else if (casService !== null && casService !== undefined && casService !== "") {
return "cas";
}
return "login";
}
@ -97,6 +101,7 @@ class AuthCallback extends React.Component {
const providerName = innerParams.get("provider");
const method = innerParams.get("method");
const samlRequest = innerParams.get("SAMLRequest");
const casService = innerParams.get("service");
const redirectUri = `${window.location.origin}/callback`;
@ -111,6 +116,31 @@ class AuthCallback extends React.Component {
redirectUri: redirectUri,
method: method,
};
if (this.getResponseType() === "cas") {
// user is using casdoor as cas sso server, and wants the ticket to be acquired
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
if (res.status === "ok") {
let msg = "Logged in successfully.";
if (casService === "") {
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
msg += "Now you can visit apps protected by Casdoor.";
}
Util.showMessage("success", msg);
if (casService !== "") {
const st = res.data;
const newUrl = new URL(casService);
newUrl.searchParams.append("ticket", st);
window.location.href = newUrl.toString();
}
} else {
Util.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
return;
}
// OAuth
const oAuthParams = Util.getOAuthGetParameters(innerParams);
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
AuthBackend.login(body, oAuthParams)

View File

@ -353,11 +353,13 @@ class ForgetPage extends React.Component {
<CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.state.application), this.state.name]}
application={application}
/>
) : (
<CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.state.application), this.state.name]}
application={application}
/>
)}
</Form.Item>

View File

@ -18,6 +18,7 @@ import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd"
import {LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
import * as AuthBackend from "./AuthBackend";
import * as OrganizationBackend from "../backend/OrganizationBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Provider from "./Provider";
import * as ProviderButton from "./ProviderButton";
@ -90,12 +91,26 @@ class LoginPage extends React.Component {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.setState({
application: application,
if (this.state.owner === null || this.state.owner === undefined || this.state.owner === "") {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.setState({
application: application,
});
});
});
} else {
OrganizationBackend.getDefaultApplication("admin", this.state.owner)
.then((res) => {
if (res.status === "ok") {
this.setState({
application: res.data,
applicationName: res.data.name,
});
} else {
Util.showMessage("error", res.msg);
}
});
}
}
getSamlApplication() {
@ -334,49 +349,54 @@ class LoginPage extends React.Component {
>
</Form.Item>
{this.renderMethodChoiceBox()}
<Form.Item
name="username"
rules={[
{
required: true,
message: i18next.t("login:Please input your username, Email or phone!"),
},
{
validator: (_, value) => {
if (this.state.isCodeSignin) {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
this.setState({validEmailOrPhone: false});
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
}
<Row style={{minHeight: 130, alignItems: "center"}}>
<Col span={24}>
<Form.Item
name="username"
rules={[
{
required: true,
message: i18next.t("login:Please input your username, Email or phone!"),
},
{
validator: (_, value) => {
if (this.state.isCodeSignin) {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
this.setState({validEmailOrPhone: false});
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
}
if (Setting.isValidPhone(this.state.username)) {
this.setState({validPhone: true});
}
if (Setting.isValidEmail(this.state.username)) {
this.setState({validEmail: true});
}
}
if (Setting.isValidPhone(this.state.username)) {
this.setState({validPhone: true});
}
if (Setting.isValidEmail(this.state.username)) {
this.setState({validEmail: true});
}
}
this.setState({validEmailOrPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.state.isCodeSignin ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
disabled={!application.enablePassword}
onChange={e => {
this.setState({
username: e.target.value,
});
}}
/>
</Form.Item>
{
this.renderPasswordOrCodeInput()
}
this.setState({validEmailOrPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
id = "input"
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.state.isCodeSignin ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
disabled={!application.enablePassword}
onChange={e => {
this.setState({
username: e.target.value,
});
}}
/>
</Form.Item>
</Col>
{
this.renderPasswordOrCodeInput()
}
</Row>
<Form.Item>
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
<Checkbox style={{float: "left"}} disabled={!application.enablePassword}>
@ -505,6 +525,10 @@ class LoginPage extends React.Component {
renderSignedInBox() {
if (this.props.account === undefined || this.props.account === null) {
if (window !== window.parent) {
const message = {tag: "Casdoor", type: "SilentSignin", data: "user-not-logged-in"};
window.parent.postMessage(message, "*");
}
return null;
}
const application = this.getApplicationObj();
@ -614,27 +638,32 @@ class LoginPage extends React.Component {
const application = this.getApplicationObj();
if (this.state.loginMethod === "password") {
return this.state.isCodeSignin ? (
<Form.Item
name="code"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
/>
</Form.Item>
<Col span={24}>
<Form.Item
name="code"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
</Col>
) : (
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("login:Password")}
disabled={!application.enablePassword}
/>
</Form.Item>
<Col span={24}>
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("login:Password")}
disabled={!application.enablePassword}
/>
</Form.Item>
</Col>
);
}
}
@ -675,29 +704,36 @@ class LoginPage extends React.Component {
);
}
const formStyle = Setting.inIframe() ? null : Setting.parseObject(application.formCss);
return (
<Row>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "80px", marginBottom: "50px", textAlign: "center"}}>
{
Setting.renderHelmet(application)
}
<CustomGithubCorner />
{
Setting.renderLogo(application)
}
{/* {*/}
{/* this.state.clientId !== null ? "Redirect" : null*/}
{/* }*/}
{
this.renderSignedInBox()
}
{
this.renderForm(application)
}
</div>
</Col>
</Row>
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}>
<CustomGithubCorner />
<Row >
<Col span={8} offset={application.formOffset === 0 || Setting.inIframe() || Setting.isMobile() ? 8 : application.formOffset} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "80px", marginBottom: "50px", textAlign: "center", ...formStyle}}>
<div>
{
Setting.renderHelmet(application)
}
{
Setting.renderLogo(application)
}
{/* {*/}
{/* this.state.clientId !== null ? "Redirect" : null*/}
{/* }*/}
{
this.renderSignedInBox()
}
{
this.renderForm(application)
}
</div>
</div>
</Col>
</Row>
</div>
);
}
}

View File

@ -177,7 +177,9 @@ export function getAuthUrl(application, provider, method) {
let endpoint = authInfo[provider.type].endpoint;
const redirectUri = `${window.location.origin}/callback`;
const scope = authInfo[provider.type].scope;
const state = Util.getQueryParamsToState(application.name, provider.name, method);
const isShortState = provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger");
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState);
if (provider.type === "Google") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;

View File

@ -360,6 +360,7 @@ class SignupPage extends React.Component {
<CountDownInput
disabled={!this.state.validEmail}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
}
@ -412,6 +413,7 @@ class SignupPage extends React.Component {
<CountDownInput
disabled={!this.state.validPhone}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
</React.Fragment>
@ -582,9 +584,9 @@ class SignupPage extends React.Component {
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp;
<a onClick={() => {
const linkInStorage = sessionStorage.getItem("signinUrl");
if(linkInStorage !== null && linkInStorage !== "") {
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLink(linkInStorage);
}else{
} else {
Setting.goToLogin(this, application);
}
}}>
@ -612,13 +614,15 @@ class SignupPage extends React.Component {
);
}
const formStyle = Setting.inIframe() ? null : Setting.parseObject(application.formCss);
return (
<div>
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}>
<CustomGithubCorner />
&nbsp;
<Row>
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
<div style={{marginTop: "10px", textAlign: "center"}}>
<Col span={8} offset={application.formOffset === 0 || Setting.inIframe() || Setting.isMobile() ? 8 : application.formOffset} style={{display: "flex", justifyContent: "center"}} >
<div style={{marginBottom: "10px", textAlign: "center", ...formStyle}}>
{
Setting.renderHelmet(application)
}

View File

@ -126,15 +126,27 @@ export function getOAuthGetParameters(params) {
}
}
export function getQueryParamsToState(applicationName, providerName, method) {
export function getStateFromQueryParams(applicationName, providerName, method, isShortState) {
let query = window.location.search;
query = `${query}&application=${applicationName}&provider=${providerName}&method=${method}`;
if (method === "link") {
query = `${query}&from=${window.location.pathname}`;
}
return btoa(query);
if (!isShortState) {
return btoa(query);
} else {
const state = providerName;
sessionStorage.setItem(state, query);
return state;
}
}
export function stateToGetQueryParams(state) {
return atob(state);
export function getQueryParamsFromState(state) {
const query = sessionStorage.getItem(state);
if (query === null) {
return atob(state);
} else {
return query;
}
}

View File

@ -0,0 +1,63 @@
// 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 getAdapters(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-adapters?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}
export function getAdapter(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-adapter?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}
export function updateAdapter(owner, name, Adapter) {
const newAdapter = Setting.deepCopy(Adapter);
return fetch(`${Setting.ServerUrl}/api/update-adapter?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newAdapter),
}).then(res => res.json());
}
export function addAdapter(Adapter) {
const newAdapter = Setting.deepCopy(Adapter);
return fetch(`${Setting.ServerUrl}/api/add-adapter`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newAdapter),
}).then(res => res.json());
}
export function deleteAdapter(Adapter) {
const newAdapter = Setting.deepCopy(Adapter);
return fetch(`${Setting.ServerUrl}/api/delete-adapter`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newAdapter),
}).then(res => res.json());
}
export function syncPolicies(owner, name) {
return fetch(`${Setting.ServerUrl}/api/sync-policies?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}

View File

@ -54,3 +54,10 @@ export function deleteOrganization(organization) {
body: JSON.stringify(newOrganization),
}).then(res => res.json());
}
export function getDefaultApplication(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-default-application?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}

View File

@ -27,15 +27,23 @@ class SingleCard extends React.Component {
};
}
wrappedAsSilentSigninLink(link) {
if (link.startsWith("http")) {
link += link.includes("?") ? "&silentSignin=1" : "?silentSignin=1";
}
return link;
}
renderCardMobile(logo, link, title, desc, time, isSingle) {
const gridStyle = {
width: "100vw",
textAlign: "center",
cursor: "pointer",
};
const silentSigninLink = this.wrappedAsSilentSigninLink(link);
return (
<Card.Grid style={gridStyle} onClick={() => Setting.goToLinkSoft(this, link)}>
<Card.Grid style={gridStyle} onClick={() => Setting.goToLinkSoft(this, silentSigninLink)}>
<img src={logo} alt="logo" height={60} style={{marginBottom: "20px"}} />
<Meta
title={title}
@ -46,6 +54,8 @@ class SingleCard extends React.Component {
}
renderCard(logo, link, title, desc, time, isSingle) {
const silentSigninLink = this.wrappedAsSilentSigninLink(link);
return (
<Col style={{paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px"}} span={6}>
<Card
@ -53,7 +63,7 @@ class SingleCard extends React.Component {
cover={
<img alt="logo" src={logo} style={{width: "100%", height: "210px", objectFit: "scale-down"}} />
}
onClick={() => Setting.goToLinkSoft(this, link)}
onClick={() => Setting.goToLinkSoft(this, silentSigninLink)}
style={isSingle ? {width: "320px"} : {width: "100%"}}
>
<Meta title={title} description={desc} />

View File

@ -17,13 +17,12 @@ import React from "react";
import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend";
import {SafetyOutlined} from "@ant-design/icons";
import {authConfig} from "../auth/Auth";
import {CaptchaWidget} from "./CaptchaWidget";
const {Search} = Input;
export const CountDownInput = (props) => {
const {disabled, textBefore, onChange, onButtonClickArgs} = props;
const {disabled, textBefore, onChange, onButtonClickArgs, application} = props;
const [visible, setVisible] = React.useState(false);
const [key, setKey] = React.useState("");
const [captchaImg, setCaptchaImg] = React.useState("");
@ -69,7 +68,7 @@ export const CountDownInput = (props) => {
};
const loadCaptcha = () => {
UserBackend.getCaptcha("admin", authConfig.appName, false).then(res => {
UserBackend.getCaptcha(application.owner, application.name, false).then(res => {
if (res.type === "none") {
UserBackend.sendCode("none", "", "", ...onButtonClickArgs).then(res => {
if (res) {

View File

@ -20,11 +20,13 @@ import de from "./locales/de/data.json";
import ko from "./locales/ko/data.json";
import ru from "./locales/ru/data.json";
import ja from "./locales/ja/data.json";
import es from "./locales/es/data.json";
import * as Conf from "./Conf";
import * as Setting from "./Setting";
const resources = {
en: en,
es: es,
zh: zh,
fr: fr,
de: de,

View File

@ -1,28 +1,44 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
font-family:
source-code-pro,
Menlo,
Monaco,
Consolas,
"Courier New",
monospace;
}
.logo {
background: url("https://cdn.casdoor.com/logo/casdoor-logo_1185x256.png");
background: url("https://cdn.casbin.org/img/casdoor-logo_1185x256.png");
background-size: 130px, 27px;
width: 130px;
height: 27px;
/*background: rgba(0, 0, 0, 0.2);*/
margin: 17px 10px 16px 20px;
margin: 17px 0 16px 15px;
float: left;
}
.ant-table.ant-table-middle .ant-table-title, .ant-table.ant-table-middle .ant-table-footer, .ant-table.ant-table-middle thead > tr > th, .ant-table.ant-table-middle tbody > tr > td {
.ant-table.ant-table-middle .ant-table-title,
.ant-table.ant-table-middle .ant-table-footer,
.ant-table.ant-table-middle thead > tr > th,
.ant-table.ant-table-middle tbody > tr > td {
padding: 1px 8px !important;
}

View File

@ -16,18 +16,19 @@ import "core-js/es";
import "react-app-polyfill/ie9";
import "react-app-polyfill/stable";
import React from "react";
import ReactDOM from "react-dom";
import {createRoot} from "react-dom/client";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import {BrowserRouter} from "react-router-dom";
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
const container = document.getElementById("root");
const app = createRoot(container);
app.render(<BrowserRouter>
<App />
</BrowserRouter>);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.

View File

@ -5,7 +5,16 @@
"My Account": "Mein Konto",
"Sign Up": "Registrieren"
},
"adapter": {
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +30,11 @@
"Enable signup": "Anmeldung aktivieren",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -100,6 +114,7 @@
"Action": "Aktion",
"Adapter": "Adapter",
"Adapter - Tooltip": "Adapter - Tooltip",
"Adapters": "Adapters",
"Add": "Neu",
"Affiliation URL": "Affiliation-URL",
"Affiliation URL - Tooltip": "Unique string-style identifier",
@ -117,6 +132,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client-IP",
"Created time": "Erstellte Zeit",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Standard Avatar",
"Default avatar - Tooltip": "default avatar",
"Delete": "Löschen",
@ -346,14 +363,20 @@
"permission": {
"Actions": "Aktionen",
"Actions - Tooltip": "Aktionen - Tooltip",
"Admin": "Admin",
"Allow": "Allow",
"Approve time": "Approve time",
"Approve time - Tooltip": "Approve time - Tooltip",
"Approved": "Approved",
"Approver": "Approver",
"Approver - Tooltip": "Approver - Tooltip",
"Deny": "Deny",
"Edit Permission": "Berechtigung bearbeiten",
"Effect": "Effekt",
"Effect - Tooltip": "Effekt - Tooltip",
"New Permission": "New Permission",
"Pending": "Pending",
"Read": "Read",
"Resource type": "Ressourcentyp",
"Resource type - Tooltip": "Ressourcentyp - Tooltip",
"Resources": "Ressourcen",
@ -361,7 +384,9 @@
"State": "State",
"State - Tooltip": "State - Tooltip",
"Submitter": "Submitter",
"Submitter - Tooltip": "Submitter - Tooltip"
"Submitter - Tooltip": "Submitter - Tooltip",
"TreeNode": "TreeNode",
"Write": "Write"
},
"product": {
"Alipay": "Alipay",
@ -649,6 +674,7 @@
"Link": "Link",
"Location": "Standort",
"Location - Tooltip": "Standort - Tooltip",
"Managed accounts": "Managed accounts",
"Modify password...": "Passwort ändern...",
"New Email": "Neue E-Mail",
"New Password": "Neues Passwort",

View File

@ -5,7 +5,16 @@
"My Account": "My Account",
"Sign Up": "Sign Up"
},
"adapter": {
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +30,11 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Enable signup - Tooltip",
"File uploaded successfully": "File uploaded successfully",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -100,6 +114,7 @@
"Action": "Action",
"Adapter": "Adapter",
"Adapter - Tooltip": "Adapter - Tooltip",
"Adapters": "Adapters",
"Add": "Add",
"Affiliation URL": "Affiliation URL",
"Affiliation URL - Tooltip": "Affiliation URL - Tooltip",
@ -117,6 +132,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Created time": "Created time",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar",
"Default avatar - Tooltip": "Default avatar - Tooltip",
"Delete": "Delete",
@ -346,14 +363,20 @@
"permission": {
"Actions": "Actions",
"Actions - Tooltip": "Actions - Tooltip",
"Admin": "Admin",
"Allow": "Allow",
"Approve time": "Approve time",
"Approve time - Tooltip": "Approve time - Tooltip",
"Approved": "Approved",
"Approver": "Approver",
"Approver - Tooltip": "Approver - Tooltip",
"Deny": "Deny",
"Edit Permission": "Edit Permission",
"Effect": "Effect",
"Effect - Tooltip": "Effect - Tooltip",
"New Permission": "New Permission",
"Pending": "Pending",
"Read": "Read",
"Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources",
@ -361,7 +384,9 @@
"State": "State",
"State - Tooltip": "State - Tooltip",
"Submitter": "Submitter",
"Submitter - Tooltip": "Submitter - Tooltip"
"Submitter - Tooltip": "Submitter - Tooltip",
"TreeNode": "TreeNode",
"Write": "Write"
},
"product": {
"Alipay": "Alipay",
@ -649,6 +674,7 @@
"Link": "Link",
"Location": "Location",
"Location - Tooltip": "Location - Tooltip",
"Managed accounts": "Managed accounts",
"Modify password...": "Modify password...",
"New Email": "New Email",
"New Password": "New Password",

View File

@ -0,0 +1,685 @@
{
"account": {
"Login": "Iniciar sesión",
"Logout": "Cerrar sesión",
"My Account": "Mi cuenta",
"Sign Up": "Registrarme"
},
"application": {
"Copy SAML metadata URL": "Copiar SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copiar URL de inicio de sesión",
"Copy signup page URL": "Copiar URL de registro de usuarios",
"Edit Application": "Editar Aplicación",
"Enable SAML compress": "Habilitar compresión SAML",
"Enable SAML compress - Tooltip": "Habilitar compresión SAML - Tooltip",
"Enable WebAuthn signin": "Habilitar inicio de sesión de WebAuthn",
"Enable WebAuthn signin - Tooltip": "Habilitar inicio de sesión de WebAuthn - Tooltip",
"Enable code signin": "Habilitar inicio de sesión por código",
"Enable code signin - Tooltip": "Habilitar inicio de sesión por código - Tooltip",
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
"Enable signup": "Habilitar nuevos registros",
"Enable signup - Tooltip": "Habilitar nuevos registros - Tooltip",
"File uploaded successfully": "El archivo ha sido subido con éxito",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "Nueva Aplicación",
"Password ON": "Constraseña ON",
"Password ON - Tooltip": "Constraseña ON - Tooltip",
"Please select a HTML file": "Seleccione un archivo HTML",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "URL de redirección",
"Redirect URLs": "URLs de redirección",
"Redirect URLs - Tooltip": "URL de redirección - Tooltip",
"Refresh token expire": "Expiración del Refresh Token",
"Refresh token expire - Tooltip": "Expiración del Refresh Token - Tooltip",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copiado al portapapeles con éxito",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "Signin session",
"Signup items": "Signup items",
"Signup items - Tooltip": "Signup items - Tooltip",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Token expire": "Expiración del Token",
"Token expire - Tooltip": "Expiración del Token - Tooltip",
"Token format": "Formato del Token",
"Token format - Tooltip": "Formato del Token - Tooltip",
"rule": "rule"
},
"cert": {
"Bit size": "Tamaño del Bit",
"Bit size - Tooltip": "Tamaño del Bit - Tooltip",
"Certificate": "Certificado",
"Certificate - Tooltip": "Certificado - Tooltip",
"Certificate copied to clipboard successfully": "Certificado copiado al portapapeles con éxito",
"Copy certificate": "Copiar certificado",
"Copy private key": "Copiar clave privada",
"Crypto algorithm": "Algoritmo criptográfico",
"Crypto algorithm - Tooltip": "Algoritmo criptográfico - Tooltip",
"Download certificate": "Descargar certificado",
"Download private key": "Descargar clave privada",
"Edit Cert": "Editar certificado",
"Expire in years": "Expiración en años",
"Expire in years - Tooltip": "Expiración en años - Tooltip",
"New Cert": "Nuevo certificado",
"Private key": "Clave privada",
"Private key - Tooltip": "Clave privada - Tooltip",
"Private key copied to clipboard successfully": "Clave privada copiada al portapapeles con éxito",
"Scope": "Alcance",
"Scope - Tooltip": "Alcance - Tooltip",
"Type": "Tipo",
"Type - Tooltip": "Tipo - Tooltip"
},
"code": {
"Code You Received": "Código que recibiste",
"Email code": "Código de Email",
"Empty Code": "Código requerido",
"Enter your code": "Ingrese su código",
"Phone code": "Código de teléfono",
"Please input your phone verification code!": "Ingrese su código de verificación teléfonico!",
"Please input your verification code!": "Ingrese su código de verificación!",
"Send Code": "Enviar código",
"Sending Code": "Enviando código",
"Submit and complete": "Enviar y completar"
},
"forget": {
"Account": "Cuenta",
"Change Password": "Cambiar contraseña",
"Choose email or phone": "Elige email o teléfono",
"Confirm": "Confirmar",
"Next Step": "Siguiente paso",
"Password": "Contraseña",
"Please input your username!": "Por favor ingrese su nombre de usuario!",
"Reset": "Reiniciar",
"Retrieve password": "Recuperar Contraseña",
"Unknown forget type": "Tipo de olvido desconocido",
"Verify": "Verificar"
},
"general": {
"Action": "Acción",
"Adapter": "Adaptador",
"Adapter - Tooltip": "Adaptador - Tooltip",
"Add": "Agregar",
"Affiliation URL": "URL de Afiliación",
"Affiliation URL - Tooltip": "URL de Afiliación - Tooltip",
"Application": "Aplicación",
"Applications": "Aplicaciones",
"Applications that require authentication": "Aplicaciones que requieren autenticación",
"Avatar": "Avatar",
"Avatar - Tooltip": "Avatar - Tooltip",
"Back Home": "Volver al inicio",
"Cancel": "Cancelar",
"Captcha": "Captcha",
"Cert": "Certificado",
"Cert - Tooltip": "Certificado - Tooltip",
"Certs": "Certificados",
"Click to Upload": "Click para subir archivo",
"Client IP": "IP del Cliente",
"Created time": "Fecha de creación",
"Default avatar": "Avatar por defecto",
"Default avatar - Tooltip": "Avatar por defecto - Tooltip",
"Delete": "Eliminar",
"Description": "Descripción",
"Description - Tooltip": "Descripción - Tooltip",
"Display name": "Apodo",
"Display name - Tooltip": "Apodo - Tooltip",
"Down": "Bajar",
"Edit": "Editar",
"Email": "Email",
"Email - Tooltip": "Email - Tooltip",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon - Tooltip",
"First name": "Nombre",
"Forget URL": "URL de olvido",
"Forget URL - Tooltip": "URL de olvido - Tooltip",
"Home": "Inicio",
"Home - Tooltip": "Inicio - Tooltip",
"ID": "ID",
"ID - Tooltip": "ID - Tooltip",
"Is enabled": "Está habilitado",
"Is enabled - Tooltip": "Está habilitado - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Apellido",
"Logo": "Logo",
"Logo - Tooltip": "Logo - Tooltip",
"Master password": "Contraseña maestra",
"Master password - Tooltip": "Contraseña maestra - Tooltip",
"Method": "Metodo",
"Model": "Modelo",
"Model - Tooltip": "Modelo - Tooltip",
"Models": "Modelos",
"Name": "Nombre",
"Name - Tooltip": "Nombre - Tooltip",
"OAuth providers": "OAuth providers",
"Organization": "Organización",
"Organization - Tooltip": "Organización - Tooltip",
"Organizations": "Organizaciones",
"Password": "Contraseña",
"Password - Tooltip": "Contraseña - Tooltip",
"Password salt": "Password salt",
"Password salt - Tooltip": "Password salt - Tooltip",
"Password type": "Tipo de Contraseña",
"Password type - Tooltip": "Tipo de Contraseña - Tooltip",
"Payments": "Pagos",
"Permissions": "Permisos",
"Permissions - Tooltip": "Permisos - Tooltip",
"Phone": "Teléfono",
"Phone - Tooltip": "Teléfono - Tooltip",
"Phone prefix": "Prefijo teléfonico",
"Phone prefix - Tooltip": "Prefijo teléfonico - Tooltip",
"Preview": "Previsualizar",
"Preview - Tooltip": "Previsualizar - Tooltip",
"Products": "Productos",
"Provider": "Proveedor",
"Provider - Tooltip": "Proveedor - Tooltip",
"Providers": "Proveedores",
"Providers - Tooltip": "Proveedores - Tooltip",
"Real name": "Nombre real",
"Records": "Registros",
"Request URI": "Request URI",
"Resources": "Recursos",
"Roles": "Roles",
"Roles - Tooltip": "Roles - Tooltip",
"Save": "Guardar",
"Save & Exit": "Guardar & Volver",
"Signin URL": "URL de inicio de sesión",
"Signin URL - Tooltip": "URL de inicio de sesión - Tooltip",
"Signup URL": "URL de registro",
"Signup URL - Tooltip": "URL de registro - Tooltip",
"Signup application": "Aplicación de inicio de sesión",
"Signup application - Tooltip": "Aplicación de inicio de sesión - Tooltip",
"Sorry, the page you visited does not exist.": "Lo sentimos, la página que visitaste no existe.",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Lo sentimos, el usuario que visitaste no existe o no estás autorizado para acceder.",
"Sorry, you do not have permission to access this page.": "Lo sentimos, no tienes permiso para acceder a esta página.",
"State": "Estado",
"State - Tooltip": "Estado - Tooltip",
"Swagger": "Swagger",
"Sync": "Sincronizador",
"Syncers": "Sincronizadores",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Subir",
"User": "Usuario",
"User - Tooltip": "Usuario - Tooltip",
"User containers": "Contenedores de usuarios",
"User type": "Tipo de usuario",
"User type - Tooltip": "Tipo de usuario - Tooltip",
"Users": "Usuarios",
"Users under all organizations": "Usuarios de todas las organizaciones",
"Webhooks": "Webhooks",
"{total} in total": "{total} en total"
},
"ldap": {
"Address": "Dirección",
"Admin": "Admin",
"Admin - Tooltip": "Admin - Tooltip",
"Admin Password": "Contraseña del admin",
"Admin Password - Tooltip": "Contraseña del admin - Tooltip",
"Auto Sync": "Sincronización automática",
"Auto Sync - Tooltip": "Sincronización automática - Tooltip",
"Base DN": "Base DN",
"Base DN - Tooltip": "Base DN - Tooltip",
"CN": "CN",
"Edit LDAP": "Editar LDAP",
"Email": "Email",
"Group Id": "Group Id",
"ID": "ID",
"Last Sync": "Última Sincronización",
"Phone": "Teléfono",
"Server": "Servidor",
"Server Host": "Host de Servidor",
"Server Host - Tooltip": "Host de Servidor - Tooltip",
"Server Name": "Nombre del Servidor",
"Server Name - Tooltip": "Nombre del Servidor - Tooltip",
"Server Port": "Puerto del servidor",
"Server Port - Tooltip": "Puerto del servidor - Tooltip",
"Sync": "Sincronizar",
"The Auto Sync option will sync all users to specify organization": "La opción de sincronización automática sincronizará todos los usuarios para la organización especificada.",
"UidNumber / Uid": "UidNumber / Uid"
},
"login": {
"Auto sign in": "Inicio de sesión automático",
"Continue with": "Continuar con",
"Email or phone": "Email o teléfono",
"Forgot password?": "¿Olvidó su contraseña?",
"Logging out...": "Cerrando su sesión...",
"No account?": "¿No estás registrado?",
"Or sign in with another account": "O inicia sesión con otra cuenta",
"Password": "Contraseña",
"Password - Tooltip": "Contraseña - Tooltip",
"Please input your code!": "¡Por favor ingrese su código!",
"Please input your password!": "¡Por favor ingrese su contraseña!",
"Please input your password, at least 6 characters!": "Su contraseña debe contener al menos 6 caracteres.",
"Please input your username, Email or phone!": "¡Ingrese su nombre de usuario, correo electrónico o teléfono!",
"Sign In": "Iniciar de sesión",
"Sign in with WebAuthn": "Iniciar de sesión con WebAuthn",
"Sign in with code": "Iniciar de sesión con código",
"Sign in with password": "Iniciar de sesión con contraseña",
"Sign in with {type}": "Iniciar de sesión con {type}",
"Signing in...": "Iniciando de sesión...",
"The input is not valid Email or Phone!": "El valor ingresado no es un Email o Teléfono válido!",
"To access": "Para ingresar",
"sign up now": "Regístrate ahora",
"username, Email or phone": "nombre de usuario, correo electrónico o teléfono"
},
"model": {
"Edit Model": "Editar Modelo",
"Model text": "Texto del modelo",
"Model text - Tooltip": "Texto del modelo - Tooltip",
"New Model": "Nuevo Modelo"
},
"organization": {
"Account items": "Items de la cuenta",
"Account items - Tooltip": "Items de la cuenta - Tooltip",
"Default avatar": "Avatar por defecto",
"Edit Organization": "Editar Organización",
"Favicon": "Favicon",
"Is profile public": "Es el perfil publico",
"Is profile public - Tooltip": "Es el perfil publico - Tooltip",
"New Organization": "Nueva Organización",
"Soft deletion": "Eliminación Soft",
"Soft deletion - Tooltip": "Eliminación Soft - Tooltip",
"Tags": "Etiquetas",
"Tags - Tooltip": "Etiquetas - Tooltip",
"Website URL": "URL del sitio web",
"Website URL - Tooltip": "URL del sitio web - Tooltip",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
},
"payment": {
"Confirm your invoice information": "Confirma la información de tu factura",
"Currency": "Moneda",
"Currency - Tooltip": "Moneda - Tooltip",
"Download Invoice": "Descargar factura",
"Edit Payment": "Editar Pago",
"Individual": "Individual",
"Invoice URL": "URL de factura",
"Invoice URL - Tooltip": "URL de factura - Tooltip",
"Invoice actions": "Acciones de factura",
"Invoice actions - Tooltip": "Acciones de factura - Tooltip",
"Invoice remark": "Comentario de factura",
"Invoice remark - Tooltip": "Comentario de factura - Tooltip",
"Invoice tax ID": "Número de identificación fiscal de la factura",
"Invoice tax ID - Tooltip": "Número de identificación fiscal de la factura - Tooltip",
"Invoice title": "Titulo de factura",
"Invoice title - Tooltip": "Titulo de factura - Tooltip",
"Invoice type": "Tipo de factura",
"Invoice type - Tooltip": "Tipo de factura - Tooltip",
"Issue Invoice": "Emitir factura",
"Message": "Mensaje",
"Message - Tooltip": "Mensaje - Tooltip",
"New Payment": "Nuevo Pago",
"Organization": "Organización",
"Person Email": "Email de la persona",
"Person Email - Tooltip": "Email de la persona - Tooltip",
"Person ID card": "Documento de identidad de la persona",
"Person ID card - Tooltip": "Documento de identidad de la persona - Tooltip",
"Person name": "Nombre de la persona",
"Person name - Tooltip": "Nombre de la persona - Tooltip",
"Person phone": "Teléfono de la persona",
"Person phone - Tooltip": "Teléfono de la persona - Tooltip",
"Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.": "Por favor revise cuidadosamente la información de su factura. Una vez emitida la factura, no podrá ser retirada ni modificada.",
"Please click the below button to return to the original website": "Haga clic en el botón de abajo para volver al sitio web original",
"Please pay the order first!": "¡Por favor pague la orden primero!",
"Price": "Precio",
"Price - Tooltip": "Precio - Tooltip",
"Processing...": "Procesando...",
"Product": "Producto",
"Product - Tooltip": "Producto - Tooltip",
"Result": "Resultado",
"Return to Website": "Volver al sitio web",
"State": "Estado",
"State - Tooltip": "Estado - Tooltip",
"The payment has failed": "El pago ha fallado",
"The payment is still under processing": "El pago aún está en proceso",
"Type": "Tipo",
"Type - Tooltip": "Tipo - Tooltip",
"You have successfully completed the payment": "Su pago se ha completado con éxito",
"please wait for a few seconds...": "por favor espere unos segundos...",
"the current state is": "el estado actual es"
},
"permission": {
"Actions": "Acciones",
"Actions - Tooltip": "Acciones - Tooltip",
"Approve time": "Fecha de aprobación",
"Approve time - Tooltip": "Fecha de aprobación - Tooltip",
"Approver": "Aprobador",
"Approver - Tooltip": "Aprobador - Tooltip",
"Edit Permission": "Editar Permiso",
"Effect": "Effect",
"Effect - Tooltip": "Effect - Tooltip",
"New Permission": "Nuevo Permiso",
"Resource type": "Tipo de recurso",
"Resource type - Tooltip": "Tipo de recurso - Tooltip",
"Resources": "Recursos",
"Resources - Tooltip": "Recursos - Tooltip",
"State": "Estado",
"State - Tooltip": "Estado - Tooltip",
"Submitter": "Submitter",
"Submitter - Tooltip": "Submitter - Tooltip"
},
"product": {
"Alipay": "Alipay",
"Buy": "Comprar",
"Buy Product": "Comprar Producto",
"CNY": "CNY",
"Currency": "Moneda",
"Currency - Tooltip": "Moneda - Tooltip",
"Detail": "Detalle",
"Detail - Tooltip": "Detalle - Tooltip",
"Edit Product": "Editar Producto",
"Image": "Imagen",
"Image - Tooltip": "Imagen - Tooltip",
"New Product": "Nuevo producto",
"Pay": "Pago",
"Payment providers": "Pasarelas de pago",
"Payment providers - Tooltip": "Pasarelas de pago - Tooltip",
"Paypal": "Paypal",
"Placing order...": "Creando su pedido...",
"Price": "Precio",
"Price - Tooltip": "Precio - Tooltip",
"Quantity": "Cantidad",
"Quantity - Tooltip": "Cantidad - Tooltip",
"Return URL": "URL de retorno",
"Return URL - Tooltip": "URL de retorno - Tooltip",
"SKU": "SKU",
"Sold": "Agotado",
"Sold - Tooltip": "Agotado - Tooltip",
"Tag": "Etiqueta",
"Tag - Tooltip": "Etiqueta - Tooltip",
"Test buy page..": "Probar página de pago...",
"There is no payment channel for this product.": "No existe un canal de pago para este producto.",
"This product is currently not in sale.": "Este producto actualmente no está a la venta.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Clave de acceso",
"Access key - Tooltip": "Clave de acceso - Tooltip",
"Agent ID": "ID del Agente",
"Agent ID - Tooltip": "ID del Agente - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"App key": "App key",
"App key - Tooltip": "App key - Tooltip",
"App secret": "App secret",
"AppSecret - Tooltip": "AppSecret - Tooltip",
"Auth URL": "Auth URL",
"Auth URL - Tooltip": "Auth URL - Tooltip",
"Bucket": "Bucket",
"Bucket - Tooltip": "Bucket - Tooltip",
"Can not parse Metadata": "Can not parse Metadata",
"Category": "Categoria",
"Category - Tooltip": "Categoria - Tooltip",
"Channel No.": "Channel No.",
"Channel No. - Tooltip": "Channel No. - Tooltip",
"Client ID": "Client ID",
"Client ID - Tooltip": "Client ID - Tooltip",
"Client ID 2": "Client ID 2",
"Client ID 2 - Tooltip": "Client ID 2 - Tooltip",
"Client secret": "Client secret",
"Client secret - Tooltip": "Client secret - Tooltip",
"Client secret 2": "Client secret 2",
"Client secret 2 - Tooltip": "Client secret 2 - Tooltip",
"Copy": "Copiar",
"Disable SSL": "Deshabilitar SSL",
"Disable SSL - Tooltip": "Deshabilitar SSL - Tooltip",
"Domain": "Dominio",
"Domain - Tooltip": "Dominio - Tooltip",
"Edit Provider": "Editar Proveedor",
"Email Content": "Contenido del Email",
"Email Content - Tooltip": "Contenido del Email - Tooltip",
"Email Title": "Titulo del Email",
"Email Title - Tooltip": "Titulo del Email - Tooltip",
"Endpoint": "Endpoint",
"Endpoint (Intranet)": "Endpoint (Intranet)",
"Host": "Host",
"Host - Tooltip": "Host - Tooltip",
"IdP": "IdP",
"IdP certificate": "IdP certificate",
"Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL - Tooltip",
"Link copied to clipboard successfully": "Enlace copiado al portapapeles con éxito",
"Metadata": "Metadata",
"Metadata - Tooltip": "Metadata - Tooltip",
"Method": "Metodo",
"Method - Tooltip": "Metodo - Tooltip",
"Name": "Nombre",
"New Provider": "Nuevo Proveedor",
"Parse": "Parsear",
"Parse Metadata successfully": "Metadata parseada con éxito",
"Port": "Puerto",
"Port - Tooltip": "Puerto - Tooltip",
"Provider URL": "URL del Proveedor",
"Provider URL - Tooltip": "URL del Proveedor - Tooltip",
"Region ID": "Region ID",
"Region ID - Tooltip": "Region ID - Tooltip",
"Region endpoint for Internet": "Region endpoint for Internet",
"Region endpoint for Intranet": "Region endpoint for Intranet",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Secret access key",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
"Send Test Email": "Enviar email de prueba",
"Sign Name": "Sign Name",
"Sign Name - Tooltip": "Sign Name - Tooltip",
"Sign request": "Sign request",
"Sign request - Tooltip": "Sign request - Tooltip",
"Signin HTML": "Signin HTML",
"Signin HTML - Edit": "Signin HTML - Edit",
"Signin HTML - Tooltip": "Signin HTML - Tooltip",
"Signup HTML": "Signup HTML",
"Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Signup HTML - Tooltip",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Código de plantilla",
"Template Code - Tooltip": "Código de plantilla - Tooltip",
"Terms of Use": "Términos de Uso",
"Terms of Use - Tooltip": "Términos de Uso - Tooltip",
"Test Connection": "Probar Conexión",
"Test Email": "Probar Email",
"Test Email - Tooltip": "Probar Email - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Tipo",
"Type - Tooltip": "Tipo - Tooltip",
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
"alertType": "alertType",
"canSignIn": "canSignIn",
"canSignUp": "canSignUp",
"canUnlink": "canUnlink",
"prompted": "prompted",
"required": "required",
"visible": "visible"
},
"record": {
"Is Triggered": "Esta activado"
},
"resource": {
"Application": "Aplicación",
"Copy Link": "Copiar Link",
"File name": "Nombre del archivo",
"File size": "Tamaño del archivo",
"Format": "Formato",
"Link copied to clipboard successfully": "Enlace copiado al portapapeles con éxito",
"Parent": "Parent",
"Tag": "Etiqueta",
"Type": "Tipo",
"Upload a file...": "Subir archivo...",
"User": "Usuario"
},
"role": {
"Edit Role": "Editar Rol",
"New Role": "Nuevo Rol",
"Sub domains": "Sub dominios",
"Sub domains - Tooltip": "Sub dominios - Tooltip",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub usuarios",
"Sub users - Tooltip": "Sub usuarios - Tooltip"
},
"signup": {
"Accept": "Aceptar",
"Agreement": "Agreement",
"Confirm": "Confirmar",
"Decline": "Rechazar",
"Have account?": "¿Ya tienes una cuenta?",
"Please accept the agreement!": "Por favor, acepte el acuerdo!",
"Please click the below button to sign in": "Por favor, haga clic en el botón de abajo para iniciar sesión",
"Please confirm your password!": "Por favor, confirme su contraseña!",
"Please input the correct ID card number!": "¡Ingrese un documento de identidad correcto!",
"Please input your Email!": "Por favor, ingrese su Email!",
"Please input your ID card number!": "Por favor, ingrese su documento de identidad!",
"Please input your address!": "Por favor, ingrese su domicilio!",
"Please input your affiliation!": "Por favor, ingrese su afiliación!",
"Please input your display name!": "Por favor, ingrese su apodo!",
"Please input your first name!": "Por favor, ingrese su nombre!",
"Please input your last name!": "Por favor, ingrese su apellido!",
"Please input your phone number!": "Por favor, ingrese su número teléfonico!",
"Please input your real name!": "Por favor, ingrese un nombre real!",
"Please select your country/region!": "Por favor, seleccione su pais/region!",
"Terms of Use": "Términos de Uso",
"The input is not invoice Tax ID!": "El valor ingresado no es un número de identificación fiscal de factura!",
"The input is not invoice title!": "El valor ingresado no es el titulo de factura!",
"The input is not valid Email!": "El valor ingresado no es un Email válido!",
"The input is not valid Phone!": "El valor ingresado no es un Teléfono válido!",
"Username": "Nombre de usuario",
"Username - Tooltip": "Nombre de usuario - Tooltip",
"Your account has been created!": "¡Tu cuenta ha sido creada!",
"Your confirmed password is inconsistent with the password!": "¡La confirmación de su contraseña es inconsistente!",
"sign in now": "iniciar sesión ahora"
},
"syncer": {
"Affiliation table": "Tabla de afiliación",
"Affiliation table - Tooltip": "Tabla de afiliación - Tooltip",
"Avatar base URL": "Avatar base URL",
"Avatar base URL - Tooltip": "Avatar base URL - Tooltip",
"Casdoor column": "Casdoor column",
"Column name": "Nombre de columna",
"Column type": "Tipo de columna",
"Database": "Base de datos",
"Database - Tooltip": "Base de datos - Tooltip",
"Database type": "Tipo de base de datos",
"Database type - Tooltip": "Tipo de base de datos - Tooltip",
"Edit Syncer": "Editar Sincronizador",
"Error text": "Texto de Error",
"Error text - Tooltip": "Texto de Error - Tooltip",
"Is hashed": "Esta hashed?",
"New Syncer": "Nuevo Sincronizador",
"Sync interval": "Intervalo de sincronización",
"Sync interval - Tooltip": "Intervalo de sincronización - Tooltip",
"Table": "Tabla",
"Table - Tooltip": "Tabla - Tooltip",
"Table columns": "Columnas de la tabla",
"Table columns - Tooltip": "Columnas de la tabla - Tooltip",
"Table primary key": "Clave primaria de la tabla",
"Table primary key - Tooltip": "Clave primaria de la tabla - Tooltip"
},
"token": {
"Access token": "Token de acceso",
"Authorization code": "Código de autorización",
"Edit Token": "Editar Token",
"Expires in": "Expira en",
"New Token": "Nuevo Token",
"Scope": "Alcance",
"Token type": "Tipo del Token"
},
"user": {
"\" + destType + \" reset": "\" + destType + \" reset",
"3rd-party logins": "3rd-party logins",
"3rd-party logins - Tooltip": "3rd-party logins - Tooltip",
"Address": "Dirección",
"Address - Tooltip": "Dirección - Tooltip",
"Affiliation": "Afiliación",
"Affiliation - Tooltip": "Afiliación - Tooltip",
"Bio": "Acerca de vos",
"Bio - Tooltip": "Acerca de vos - Tooltip",
"Cancel": "Cancelar",
"Captcha Verify Failed": "Fallo la verificación del Captcha",
"Captcha Verify Success": "Captcha verificado con éxito",
"Code Sent": "Código enviado",
"Country/Region": "Pais/Región",
"Country/Region - Tooltip": "Pais/Región - Tooltip",
"Edit User": "Editar usuario",
"Empty input!": "Campo requerido!",
"Homepage": "Página de Inicio",
"Homepage - Tooltip": "Página de Inicio - Tooltip",
"ID card": "Documento de identidad",
"Input your email": "Ingrese su email",
"Input your phone number": "Ingrese su número teléfonico",
"Is admin": "Es admin",
"Is admin - Tooltip": "Es admin - Tooltip",
"Is deleted": "Esta Eliminado",
"Is deleted - Tooltip": "Esta Eliminado - Tooltip",
"Is forbidden": "Está prohibido",
"Is forbidden - Tooltip": "Está prohibido - Tooltip",
"Is global admin": "Es admin global",
"Is global admin - Tooltip": "Es admin global - Tooltip",
"Link": "Enlace",
"Location": "Ubicación",
"Location - Tooltip": "Ubicación - Tooltip",
"Modify password...": "Cambiar contraseña...",
"New Email": "Nuevo Email",
"New Password": "Nueva Contraseña",
"New User": "Nuevo usuario",
"New phone": "Nuevo teléfono",
"OK": "OK",
"Old Password": "Contraseña anterior",
"Password": "Contraseña",
"Password Set": "Password Set",
"Properties": "Propiedades",
"Re-enter New": "Reingrese de nuevo",
"Reset Email...": "Cambiar Email...",
"Reset Phone...": "Cambiar Phone...",
"Select a photo...": "Select a photo...",
"Set Password": "Cambiar contraseña",
"Set new profile picture": "Cambiar imagen de perfil",
"Set password...": "Cambiando contraseña...",
"Tag": "Etiqueta",
"Tag - Tooltip": "Etiqueta - Tooltip",
"Title": "Titulo",
"Title - Tooltip": "Titulo - Tooltip",
"Two passwords you typed do not match.": "La contraseña no coincide.",
"Unlink": "Desvincular",
"Upload (.xlsx)": "Subir archivo (.xlsx)",
"Upload a photo": "Subir foto",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "input password"
},
"webhook": {
"Content type": "Tipo de contenido",
"Content type - Tooltip": "Tipo de contenido - Tooltip",
"Edit Webhook": "Editar Webhook",
"Events": "Eventos",
"Events - Tooltip": "Eventos - Tooltip",
"Headers": "Cabeceras",
"Headers - Tooltip": "Cabeceras - Tooltip",
"Is user extended": "Es usuario extendido",
"Is user extended - Tooltip": "Es usuario extendido - Tooltip",
"Method": "Metodo",
"Method - Tooltip": "Metodo - Tooltip",
"Name": "Nombre",
"New Webhook": "Nuevo Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Value": "Valor"
}
}

View File

@ -5,7 +5,16 @@
"My Account": "Mon Compte",
"Sign Up": "S'inscrire"
},
"adapter": {
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +30,11 @@
"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",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -100,6 +114,7 @@
"Action": "Action",
"Adapter": "Adapter",
"Adapter - Tooltip": "Adapter - Tooltip",
"Adapters": "Adapters",
"Add": "Ajouter",
"Affiliation URL": "URL d'affiliation",
"Affiliation URL - Tooltip": "Unique string-style identifier",
@ -117,6 +132,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "IP du client",
"Created time": "Date de création",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Avatar par défaut",
"Default avatar - Tooltip": "default avatar",
"Delete": "Supprimez",
@ -346,14 +363,20 @@
"permission": {
"Actions": "Actions",
"Actions - Tooltip": "Actions - Info-bulle",
"Admin": "Admin",
"Allow": "Allow",
"Approve time": "Approve time",
"Approve time - Tooltip": "Approve time - Tooltip",
"Approved": "Approved",
"Approver": "Approver",
"Approver - Tooltip": "Approver - Tooltip",
"Deny": "Deny",
"Edit Permission": "Autorisation d'édition",
"Effect": "Effet",
"Effect - Tooltip": "Effet - Infobulle",
"New Permission": "New Permission",
"Pending": "Pending",
"Read": "Read",
"Resource type": "Type de ressource",
"Resource type - Tooltip": "Type de ressource - infobulle",
"Resources": "Ressource",
@ -361,7 +384,9 @@
"State": "State",
"State - Tooltip": "State - Tooltip",
"Submitter": "Submitter",
"Submitter - Tooltip": "Submitter - Tooltip"
"Submitter - Tooltip": "Submitter - Tooltip",
"TreeNode": "TreeNode",
"Write": "Write"
},
"product": {
"Alipay": "Alipay",
@ -649,6 +674,7 @@
"Link": "Lier",
"Location": "Localisation",
"Location - Tooltip": "Localisation - Infobulle",
"Managed accounts": "Managed accounts",
"Modify password...": "Modifier le mot de passe...",
"New Email": "Nouvel e-mail",
"New Password": "Nouveau mot de passe",

View File

@ -5,7 +5,16 @@
"My Account": "マイアカウント",
"Sign Up": "新規登録"
},
"adapter": {
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +30,11 @@
"Enable signup": "サインアップを有効にする",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "ファイルが正常にアップロードされました",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -100,6 +114,7 @@
"Action": "アクション",
"Adapter": "Adapter",
"Adapter - Tooltip": "Adapter - Tooltip",
"Adapters": "Adapters",
"Add": "追加",
"Affiliation URL": "アフィリエイトURL",
"Affiliation URL - Tooltip": "Unique string-style identifier",
@ -117,6 +132,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "クライアント IP",
"Created time": "作成日時",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "デフォルトのアバター",
"Default avatar - Tooltip": "default avatar",
"Delete": "削除",
@ -346,14 +363,20 @@
"permission": {
"Actions": "アクション",
"Actions - Tooltip": "アクション → ツールチップ",
"Admin": "Admin",
"Allow": "Allow",
"Approve time": "Approve time",
"Approve time - Tooltip": "Approve time - Tooltip",
"Approved": "Approved",
"Approver": "Approver",
"Approver - Tooltip": "Approver - Tooltip",
"Deny": "Deny",
"Edit Permission": "権限を編集",
"Effect": "効果",
"Effect - Tooltip": "エフェクト - ツールチップ",
"New Permission": "New Permission",
"Pending": "Pending",
"Read": "Read",
"Resource type": "リソースタイプ",
"Resource type - Tooltip": "リソースタイプ - ツールチップ",
"Resources": "リソース",
@ -361,7 +384,9 @@
"State": "State",
"State - Tooltip": "State - Tooltip",
"Submitter": "Submitter",
"Submitter - Tooltip": "Submitter - Tooltip"
"Submitter - Tooltip": "Submitter - Tooltip",
"TreeNode": "TreeNode",
"Write": "Write"
},
"product": {
"Alipay": "Alipay",
@ -649,6 +674,7 @@
"Link": "リンク",
"Location": "場所",
"Location - Tooltip": "場所 → ツールチップ",
"Managed accounts": "Managed accounts",
"Modify password...": "パスワードを変更...",
"New Email": "新しいメール",
"New Password": "新しいパスワード",

View File

@ -5,7 +5,16 @@
"My Account": "My Account",
"Sign Up": "Sign Up"
},
"adapter": {
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +30,11 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "File uploaded successfully",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -100,6 +114,7 @@
"Action": "Action",
"Adapter": "Adapter",
"Adapter - Tooltip": "Adapter - Tooltip",
"Adapters": "Adapters",
"Add": "Add",
"Affiliation URL": "Affiliation URL",
"Affiliation URL - Tooltip": "Unique string-style identifier",
@ -117,6 +132,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Created time": "Created time",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar",
"Default avatar - Tooltip": "default avatar",
"Delete": "Delete",
@ -346,14 +363,20 @@
"permission": {
"Actions": "Actions",
"Actions - Tooltip": "Actions - Tooltip",
"Admin": "Admin",
"Allow": "Allow",
"Approve time": "Approve time",
"Approve time - Tooltip": "Approve time - Tooltip",
"Approved": "Approved",
"Approver": "Approver",
"Approver - Tooltip": "Approver - Tooltip",
"Deny": "Deny",
"Edit Permission": "Edit Permission",
"Effect": "Effect",
"Effect - Tooltip": "Effect - Tooltip",
"New Permission": "New Permission",
"Pending": "Pending",
"Read": "Read",
"Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources",
@ -361,7 +384,9 @@
"State": "State",
"State - Tooltip": "State - Tooltip",
"Submitter": "Submitter",
"Submitter - Tooltip": "Submitter - Tooltip"
"Submitter - Tooltip": "Submitter - Tooltip",
"TreeNode": "TreeNode",
"Write": "Write"
},
"product": {
"Alipay": "Alipay",
@ -649,6 +674,7 @@
"Link": "Link",
"Location": "Location",
"Location - Tooltip": "Location - Tooltip",
"Managed accounts": "Managed accounts",
"Modify password...": "Modify password...",
"New Email": "New Email",
"New Password": "New Password",

View File

@ -5,7 +5,16 @@
"My Account": "Мой аккаунт",
"Sign Up": "Регистрация"
},
"adapter": {
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Копировать адрес метаданных SAML",
"Copy prompt page URL": "Скопировать URL-адрес страницы запроса",
"Copy signin page URL": "Скопировать URL-адрес страницы входа",
@ -21,6 +30,11 @@
"Enable signup": "Включить регистрацию",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Файл успешно загружен",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Виды грантов",
"Grant types - Tooltip": "Виды грантов - Подсказка",
"New Application": "Новое приложение",
@ -100,6 +114,7 @@
"Action": "Действие",
"Adapter": "Adapter",
"Adapter - Tooltip": "Adapter - Tooltip",
"Adapters": "Adapters",
"Add": "Добавить",
"Affiliation URL": "URL-адрес партнёра",
"Affiliation URL - Tooltip": "Unique string-style identifier",
@ -117,6 +132,8 @@
"Click to Upload": "Нажмите здесь, чтобы загрузить",
"Client IP": "IP клиента",
"Created time": "Время создания",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Аватар по умолчанию",
"Default avatar - Tooltip": "default avatar",
"Delete": "Удалить",
@ -346,14 +363,20 @@
"permission": {
"Actions": "Действия",
"Actions - Tooltip": "Действия - Подсказка",
"Admin": "Admin",
"Allow": "Allow",
"Approve time": "Approve time",
"Approve time - Tooltip": "Approve time - Tooltip",
"Approved": "Approved",
"Approver": "Approver",
"Approver - Tooltip": "Approver - Tooltip",
"Deny": "Deny",
"Edit Permission": "Изменить права доступа",
"Effect": "Эффект",
"Effect - Tooltip": "Эффект - Подсказка",
"New Permission": "New Permission",
"Pending": "Pending",
"Read": "Read",
"Resource type": "Тип ресурса",
"Resource type - Tooltip": "Тип ресурса - Подсказка",
"Resources": "Ресурсы",
@ -361,7 +384,9 @@
"State": "State",
"State - Tooltip": "State - Tooltip",
"Submitter": "Submitter",
"Submitter - Tooltip": "Submitter - Tooltip"
"Submitter - Tooltip": "Submitter - Tooltip",
"TreeNode": "TreeNode",
"Write": "Write"
},
"product": {
"Alipay": "Alipay",
@ -649,6 +674,7 @@
"Link": "Ссылка",
"Location": "Местоположение",
"Location - Tooltip": "Расположение - Подсказка",
"Managed accounts": "Managed accounts",
"Modify password...": "Изменить пароль...",
"New Email": "Новое письмо",
"New Password": "Новый пароль",

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