Compare commits

...

41 Commits

Author SHA1 Message Date
31b586e391 feat: Add email config test on provider edit page (#819)
* feat: Add email config test on provider edit page

* Re-use send-email API

* Optimize code

Optimize code

* Update service.go

* Update service.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-24 01:47:10 +08:00
249f83e764 Fix TestProduct() compile error. 2022-06-23 00:54:31 +08:00
16f5569e50 fix: encryption without salt (#821)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-06-22 22:30:27 +08:00
f99c1f44e8 fix: don't trigger countdown if failed to send verification code (#815)
* feat: add countdown when no captcha provider found

* fix: add countdown when sent code successfully
2022-06-22 22:22:40 +08:00
c8c4dfbfb8 Fix bug and i18n issue in captcha provider edit page. 2022-06-22 21:54:25 +08:00
d9c6ff2507 fix: captcha widget JS warnings (#820) 2022-06-22 18:31:18 +08:00
e1664f2f60 Fix newApplication() to add provider. 2022-06-22 00:08:46 +08:00
460a4d4969 fix: init default captcha provider (#810)
* feat: init built in provider

* Update built-in provider in application

* Delete unnecessary judge

* Update init.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-22 00:03:55 +08:00
376bac15dc fix: improve swagger Api docunment (#812) 2022-06-21 23:11:29 +08:00
8d0e92edef Fix missing items in renderAccountItem(). 2022-06-21 17:08:08 +08:00
0075b7af52 Fix JS warnings. 2022-06-21 15:26:58 +08:00
2c57bece39 feat: fix stuck error when no captcha provider found (#808) 2022-06-21 12:22:46 +08:00
2e42511bc4 feat: support configurable captcha(reCaptcha & hCaptcha) (#765)
* feat: support configurable captcha(layered architecture)

* refactor & add captcha logo

* rename captcha

* Update authz.go

* Update hcaptcha.go

* Update default.go

* Update recaptcha.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-18 16:00:31 +08:00
ae4ab9902b Add accountTable. 2022-06-18 01:41:21 +08:00
065b235dc5 Fix signupTable i18n. 2022-06-17 23:26:02 +08:00
63c09a879f fix: disable jsx-a11y/anchor-is-valid (#800)
* fix: disable jsx-a11y/anchor-is-valid

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

* Update LoginPage.js

* Update SignupPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-17 19:57:11 +08:00
61c80e790f Fix Authentication failure! invalid_ticket: OneLogin::RubySaml::ValidationError #798 (#799) 2022-06-17 18:35:44 +08:00
be91ff47aa Fix logo columns. 2022-06-17 00:07:16 +08:00
b4c18eb7a4 Use codemirror for samlMetadata. 2022-06-16 23:59:18 +08:00
0f483fb65b Improve preview buttons to copy link. 2022-06-16 22:01:09 +08:00
ebe9889d58 Improve i18n 2022-06-16 21:35:52 +08:00
ee42fcac8e Remove signup_item.go 2022-06-16 20:52:54 +08:00
6187b48f61 fix: show alert when user clicks on application edit page's preview window (#794)
* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* Update ApplicationEditPage.js

* fix: show alert when user clicks on application edit page's preview window

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-15 22:11:37 +08:00
2020955270 Fix cannot support old Docker version bug, revert PR: https://github.com/casdoor/casdoor/pull/606 2022-06-15 01:20:00 +08:00
1b5a8f8e57 Fix missing i18n text. 2022-06-15 00:55:06 +08:00
ff94e5164a feat: fix incorrect CAS url concatenation (#795)
* fix: fix incorrect cas url concatenation

* Update LoginPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-14 21:51:40 +08:00
15a6fd2b52 feat: show alert when user clicks on application edit page's preview wi… (#791)
* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* Update ApplicationEditPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-13 12:18:18 +08:00
37b6b50751 fix: remove redundant query for OAuth user (#788) 2022-06-10 15:58:22 +08:00
efe5431f54 fix: OAuth user id confusion caused by username (#785) 2022-06-10 00:08:26 +08:00
e9159902eb fix: fix the web compiled warnings (#778)
* fix: fix the web compiled warnings

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

* fix: disable changeMomentLanguage

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

* Update SyncerEditPage.js

* Update UserEditPage.js

* Update ResourceListPage.js

* Update ProviderEditPage.js

* Update ProductBuyPage.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-06-05 20:56:31 +08:00
604e2757c8 fix: fix the problem that user owner is not updated when updating organization name (#775)
* fix: use openid or unionid as username rather than nickname when logging with WeChat
FIX #762

* fix: fix the problem that user owner is not updated when updating organization name

* Update wechat.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-03 00:37:22 +08:00
88c5aae9e9 Fix meta desc info. 2022-06-01 22:22:00 +08:00
3d0cf8788b fix: trigger missing webhook (#770)
* fix: trigger missing webhook

* Update auth.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-01 09:34:56 +08:00
e78ea2546f fix: bilibili name and avatar (#772)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-31 21:54:00 +08:00
f7705931f7 fix: handle WeChat username conflicts (#771)
* handle username conflicts

* Update auth.go

Co-authored-by: roobtyan <roobtyan@qq.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-05-31 21:51:41 +08:00
5d8b710bf7 fix: use openid or unionid as username rather than nickname when logging with WeChat (#763)
FIX #762
2022-05-31 21:22:10 +08:00
b85ad896bf fix: saml endpoint crash (#773)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-31 21:10:35 +08:00
42c2210178 fix: set phone prefix when disable verification code (#769)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-30 18:26:42 +08:00
d52caed3a9 feat: add model page (#757)
* feat: add model page

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

* feat: support config model for permission

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

* translation and indentation

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-24 18:27:47 +08:00
27d8cd758d Simplify README 2022-05-23 21:45:31 +08:00
98f77960de feat: add Douyin OAuth provider (#753) 2022-05-15 20:59:21 +08:00
81 changed files with 4028 additions and 758 deletions

0
$env Normal file
View File

152
README.md
View File

@ -42,166 +42,66 @@
</a> </a>
</p> </p>
## Online demo ## Online demo
Deployed site: https://door.casdoor.com/ - 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)
## Quick Start
Run your own casdoor program in a few minutes.
### Download
There are two methods, get code via go subcommand `get`: ## Documentation
```shell - International: https://casdoor.org
go get github.com/casdoor/casdoor - Asian mirror: https://docs.casdoor.cn
```
or `git`:
```bash
git clone https://github.com/casdoor/casdoor
```
Finally, change directory: ## Install
```bash - By source code: https://casdoor.org/docs/basic/server-installation
cd casdoor/ - By Docker: https://casdoor.org/docs/basic/try-with-docker
```
We provide two start up methods for all kinds of users.
### Manual
#### Simple configuration ## How to connect to Casdoor?
Casdoor requires a running Relational database to be operational.Thus you need to modify configuration to point out the location of database.
Edit `conf/app.conf`, modify `dataSourceName` to correct database info, which follows this format: https://casdoor.org/docs/how-to-connect/overview
```bash
username:password@tcp(database_ip:database_port)/
```
Then create an empty schema (database) named `casdoor` in your relational database. After the program runs for the first time, it will automatically create tables in this schema.
You can also edit `main.go`, modify `false` to `true`. It will automatically create the schema (database) named `casdoor` in this database. ## Casdoor Public API
```bash - Docs: https://casdoor.org/docs/basic/public-api
createDatabase := flag.Bool("createDatabase", false, "true if you need casdoor to create database") - Swagger: https://door.casdoor.com/swagger
```
#### Run
Casdoor provides two run modes, the difference is binary size and user prompt.
##### Dev Mode ## Integrations
Edit `conf/app.conf`, set `runmode=dev`. Firstly build front-end files: https://casdoor.org/docs/integration/apisix
```bash
cd web/ && yarn && yarn run start
```
*❗ A word of caution ❗: Casdoor's front-end is built using yarn. You should use `yarn` instead of `npm`. It has a potential failure during building the files if you use `npm`.*
Then build back-end binary file, change directory to root(Relative to casdoor): ## How to contact?
```bash - Gitter: https://gitter.im/casbin/casdoor
go run main.go - Forum: https://forum.casbin.com
``` - Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e
That's it! Try to visit http://127.0.0.1:7001/. :small_airplane:
**But make sure you always request the backend port 8000 when you are using SDKs.**
##### Production Mode
Edit `conf/app.conf`, set `runmode=prod`. Firstly build front-end files:
```bash
cd web/ && yarn && yarn run build
```
Then build back-end binary file, change directory to root(Relative to casdoor):
```bash
go build main.go && sudo ./main
```
> Notice, you should visit back-end port, default 8000. Now try to visit **http://SERVER_IP:8000/**
### Docker
Casdoor provide 2 kinds of image:
- casbin/casdoor-all-in-one, in which casdoor binary, a mysql database and all necessary configurations are packed up. This image is for new user to have a trial on casdoor quickly. **With this image you can start a casdoor immediately with one single command (or two) without any complex configuration**. **Note: we DO NOT recommend you to use this image in productive environment**
- casbin/casdoor: normal & graceful casdoor image with only casdoor and environment installed.
This method requires [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) to be installed first.
### Start casdoor with casbin/casdoor-all-in-one
if the image is not pulled, pull it from dockerhub
```shell
docker pull casbin/casdoor-all-in-one
```
Start it with
```shell
docker run -p 8000:8000 casbin/casdoor-all-in-one
```
Now you can visit http://localhost:8000 and have a try. Default account and password is 'admin' and '123'. Go for it!
### Start casdoor with casbin/casdoor
#### modify the configurations
For the convenience of your first attempt, docker-compose.yml contains commands to start a database via docker.
Thus edit `conf/app.conf` to point out the location of database(db:3306), modify `dataSourceName` to the fixed content:
```bash
dataSourceName = root:123456@tcp(db:3306)/
```
> If you need to modify `conf/app.conf`, you need to re-run `docker-compose up`.
#### Run
```bash
docker-compose up
```
### K8S
You could use helm to deploy casdoor in k8s. At first, you should modify the [configmap](./manifests/casdoor/templates/configmap.yaml) for your application.
And then run bellow command to deploy it.
```bash
IMG_TAG=latest make deploy
```
And undeploy it with:
```bash
make undeploy
```
That's it! Try to visit http://localhost:8000/. :small_airplane:
## Detailed documentation
We also provide a complete [document](https://casdoor.org/) as a reference.
## Other examples
These all use casdoor as a centralized authentication platform.
- [Casnode](https://github.com/casbin/casnode): Next-generation forum software based on React + Golang.
- [Casbin-OA](https://github.com/casbin/casbin-oa): A full-featured OA(Office Assistant) system.
- ......
## Contribute ## 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). 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 notice ### I18n translation
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-web) 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-web) 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 ## License
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE) [Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)

View File

@ -95,7 +95,8 @@ p, *, *, GET, /api/get-providers, *, *
p, *, *, POST, /api/unlink, *, * p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, * p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, * p, *, *, POST, /api/send-verification-code, *, *
p, *, *, GET, /api/get-human-check, *, * p, *, *, GET, /api/get-captcha, *, *
p, *, *, POST, /api/verify-captcha, *, *
p, *, *, POST, /api/reset-email-or-phone, *, * p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, * p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, * p, *, *, GET, /.well-known/openid-configuration, *, *

View File

@ -1,4 +1,4 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved. // Copyright 2022 The Casdoor Authors. All Rights Reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,12 +12,18 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package object package captcha
type SignupItem struct { import "github.com/casdoor/casdoor/object"
Name string `json:"name"`
Visible bool `json:"visible"` type DefaultCaptchaProvider struct {
Required bool `json:"required"` }
Prompted bool `json:"prompted"`
Rule string `json:"rule"` func NewDefaultCaptchaProvider() *DefaultCaptchaProvider {
captcha := &DefaultCaptchaProvider{}
return captcha
}
func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
return object.VerifyCaptcha(clientSecret, token), nil
} }

60
captcha/hcaptcha.go Normal file
View File

@ -0,0 +1,60 @@
// 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 captcha
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)
const HCaptchaVerifyUrl = "https://hcaptcha.com/siteverify"
type HCaptchaProvider struct {
}
func NewHCaptchaProvider() *HCaptchaProvider {
captcha := &HCaptchaProvider{}
return captcha
}
func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},
}
resp, err := http.PostForm(HCaptchaVerifyUrl, reqData)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, err
}
type captchaResponse struct {
Success bool `json:"success"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
return captchaResp.Success, nil
}

30
captcha/provider.go Normal file
View File

@ -0,0 +1,30 @@
// 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 captcha
type CaptchaProvider interface {
VerifyCaptcha(token, clientSecret string) (bool, error)
}
func GetCaptchaProvider(captchaType string) CaptchaProvider {
if captchaType == "Default" {
return NewDefaultCaptchaProvider()
} else if captchaType == "reCAPTCHA" {
return NewReCaptchaProvider()
} else if captchaType == "hCaptcha" {
return NewHCaptchaProvider()
}
return nil
}

60
captcha/recaptcha.go Normal file
View File

@ -0,0 +1,60 @@
// 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 captcha
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)
const ReCaptchaVerifyUrl = "https://recaptcha.net/recaptcha/api/siteverify"
type ReCaptchaProvider struct {
}
func NewReCaptchaProvider() *ReCaptchaProvider {
captcha := &ReCaptchaProvider{}
return captcha
}
func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},
}
resp, err := http.PostForm(ReCaptchaVerifyUrl, reqData)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, err
}
type captchaResponse struct {
Success bool `json:"success"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
return captchaResp.Success, nil
}

View File

@ -17,6 +17,7 @@ package conf
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"strconv" "strconv"
"strings" "strings"
@ -61,7 +62,12 @@ func GetBeegoConfDataSourceName() string {
runningInDocker := os.Getenv("RUNNING_IN_DOCKER") runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
if runningInDocker == "true" { if runningInDocker == "true" {
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "host.docker.internal") // https://stackoverflow.com/questions/48546124/what-is-linux-equivalent-of-host-docker-internal
if runtime.GOOS == "linux" {
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "172.17.0.1")
} else {
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "host.docker.internal")
}
} }
return dataSourceName return dataSourceName

View File

@ -75,12 +75,14 @@ type Response struct {
Data2 interface{} `json:"data2"` Data2 interface{} `json:"data2"`
} }
type HumanCheck struct { type Captcha struct {
Type string `json:"type"` Type string `json:"type"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
Scene string `json:"scene"` Scene string `json:"scene"`
CaptchaId string `json:"captchaId"` CaptchaId string `json:"captchaId"`
CaptchaImage interface{} `json:"captchaImage"` CaptchaImage []byte `json:"captchaImage"`
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
} }
// Signup // Signup
@ -291,20 +293,30 @@ func (c *ApiController) GetUserinfo() {
c.ServeJSON() c.ServeJSON()
} }
// GetHumanCheck ... // GetCaptcha ...
// @Tag Login API // @Tag Login API
// @Title GetHumancheck // @Title GetCaptcha
// @router /api/get-human-check [get] // @router /api/get-captcha [get]
func (c *ApiController) GetHumanCheck() { func (c *ApiController) GetCaptcha() {
c.Data["json"] = HumanCheck{Type: "none"} applicationId := c.Input().Get("applicationId")
isCurrentProvider := c.Input().Get("isCurrentProvider")
provider := object.GetDefaultHumanCheckProvider() captchaProvider, err := object.GetCaptchaProviderByApplication(applicationId, isCurrentProvider)
if provider == nil { if err != nil {
id, img := object.GetCaptcha() c.ResponseError(err.Error())
c.Data["json"] = HumanCheck{Type: "captcha", CaptchaId: id, CaptchaImage: img}
c.ServeJSON()
return return
} }
c.ServeJSON() if captchaProvider != nil {
if captchaProvider.Type == "Default" {
id, img := object.GetCaptcha()
c.ResponseOk(Captcha{Type: captchaProvider.Type, CaptchaId: id, CaptchaImage: img})
return
} else if captchaProvider.Type != "" {
c.ResponseOk(Captcha{Type: captchaProvider.Type, ClientId: captchaProvider.ClientId, ClientSecret: captchaProvider.ClientSecret})
return
}
}
c.ResponseOk(Captcha{Type: "none"})
} }

View File

@ -28,6 +28,7 @@ import (
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/google/uuid"
) )
func codeToResponse(code *object.Code) *Response { func codeToResponse(code *object.Code) *Response {
@ -132,7 +133,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
// @Param redirectUri query string true "redirect uri" // @Param redirectUri query string true "redirect uri"
// @Param scope query string true "scope" // @Param scope query string true "scope"
// @Param state query string true "state" // @Param state query string true "state"
// @Success 200 {object} controllers.api_controller.Response The Response object // @Success 200 {object} Response The Response object
// @router /get-app-login [get] // @router /get-app-login [get]
func (c *ApiController) GetApplicationLogin() { func (c *ApiController) GetApplicationLogin() {
clientId := c.Input().Get("clientId") clientId := c.Input().Get("clientId")
@ -162,9 +163,16 @@ func setHttpClient(idProvider idp.IdProvider, providerType string) {
// @Title Login // @Title Login
// @Tag Login API // @Tag Login API
// @Description login // @Description login
// @Param oAuthParams query string true "oAuth parameters" // @Param clientId query string true clientId
// @Param body body RequestForm true "Login information" // @Param responseType query string true responseType
// @Success 200 {object} controllers.api_controller.Response The Response object // @Param redirectUri query string true redirectUri
// @Param scope query string false scope
// @Param state query string false state
// @Param nonce query string false nonce
// @Param code_challenge_method query string false code_challenge_method
// @Param code_challenge query string false code_challenge
// @Param form body controllers.RequestForm true "Login information"
// @Success 200 {object} Response The Response object
// @router /login [post] // @router /login [post]
func (c *ApiController) Login() { func (c *ApiController) Login() {
resp := &Response{} resp := &Response{}
@ -222,7 +230,11 @@ func (c *ApiController) Login() {
} }
// disable the verification code // disable the verification code
object.DisableVerificationCode(form.Username) if strings.Contains(form.Username, "@") {
object.DisableVerificationCode(form.Username)
} else {
object.DisableVerificationCode(fmt.Sprintf("+%s%s", form.PhonePrefix, form.Username))
}
user = object.GetUserByFields(form.Organization, form.Username) user = object.GetUserByFields(form.Organization, form.Username)
if user == nil { if user == nil {
@ -248,7 +260,7 @@ func (c *ApiController) Login() {
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
util.SafeGoroutine(func() {object.AddRecord(record)}) util.SafeGoroutine(func() { object.AddRecord(record) })
} }
} else if form.Provider != "" { } else if form.Provider != "" {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
@ -321,12 +333,6 @@ func (c *ApiController) Login() {
user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id)) user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id))
} else if provider.Category == "OAuth" { } else if provider.Category == "OAuth" {
user = object.GetUserByField(application.Organization, provider.Type, userInfo.Id) user = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if user == nil {
user = object.GetUserByField(application.Organization, provider.Type, userInfo.Username)
}
if user == nil {
user = object.GetUserByField(application.Organization, "name", userInfo.Username)
}
} }
if user != nil && user.IsDeleted == false { if user != nil && user.IsDeleted == false {
@ -341,7 +347,7 @@ func (c *ApiController) Login() {
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
util.SafeGoroutine(func() {object.AddRecord(record)}) util.SafeGoroutine(func() { object.AddRecord(record) })
} else if provider.Category == "OAuth" { } else if provider.Category == "OAuth" {
// Sign up via OAuth // Sign up via OAuth
if !application.EnableSignUp { if !application.EnableSignUp {
@ -354,6 +360,19 @@ func (c *ApiController) Login() {
return return
} }
// Handle username conflicts
tmpUser := object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Username))
if tmpUser != nil {
uid, err := uuid.NewRandom()
if err != nil {
c.ResponseError(err.Error())
return
}
uidStr := strings.Split(uid.String(), "-")
userInfo.Username = fmt.Sprintf("%s_%s", userInfo.Username, uidStr[1])
}
properties := map[string]string{} properties := map[string]string{}
properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2) properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2)
user = &object.User{ user = &object.User{
@ -390,7 +409,13 @@ func (c *ApiController) Login() {
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
util.SafeGoroutine(func() {object.AddRecord(record)}) util.SafeGoroutine(func() { object.AddRecord(record) })
record2 := object.NewRecord(c.Ctx)
record2.Action = "signup"
record2.Organization = application.Organization
record2.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record2) })
} else if provider.Category == "SAML" { } else if provider.Category == "SAML" {
resp = &Response{Status: "error", Msg: "The account does not exist"} resp = &Response{Status: "error", Msg: "The account does not exist"}
} }
@ -403,9 +428,6 @@ func (c *ApiController) Login() {
} }
oldUser := object.GetUserByField(application.Organization, provider.Type, userInfo.Id) oldUser := object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if oldUser == nil {
oldUser = object.GetUserByField(application.Organization, provider.Type, userInfo.Username)
}
if oldUser != nil { if oldUser != nil {
c.ResponseError(fmt.Sprintf("The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)", provider.Type, userInfo.Username, userInfo.DisplayName, oldUser.Name, oldUser.DisplayName)) c.ResponseError(fmt.Sprintf("The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)", provider.Type, userInfo.Username, userInfo.DisplayName, oldUser.Name, oldUser.DisplayName))
return return
@ -434,6 +456,11 @@ func (c *ApiController) Login() {
user := c.getCurrentUser() user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &form) resp = c.HandleLoggedIn(application, user, &form)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else { } else {
c.ResponseError(fmt.Sprintf("unknown authentication type (not password or provider), form = %s", util.StructToJson(form))) c.ResponseError(fmt.Sprintf("unknown authentication type (not password or provider), form = %s", util.StructToJson(form)))
return return

120
controllers/model.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2021 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"
)
// GetModels
// @Title GetModels
// @Tag Model API
// @Description get models
// @Param owner query string true "The owner of models"
// @Success 200 {array} object.Model The Response object
// @router /get-models [get]
func (c *ApiController) GetModels() {
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.GetModels(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetModelCount(owner, field, value)))
models := object.GetPaginationModels(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(models, paginator.Nums())
}
}
// GetModel
// @Title GetModel
// @Tag Model API
// @Description get model
// @Param id query string true "The id of the model"
// @Success 200 {object} object.Model The Response object
// @router /get-model [get]
func (c *ApiController) GetModel() {
id := c.Input().Get("id")
c.Data["json"] = object.GetModel(id)
c.ServeJSON()
}
// UpdateModel
// @Title UpdateModel
// @Tag Model API
// @Description update model
// @Param id query string true "The id of the model"
// @Param body body object.Model true "The details of the model"
// @Success 200 {object} controllers.Response The Response object
// @router /update-model [post]
func (c *ApiController) UpdateModel() {
id := c.Input().Get("id")
var model object.Model
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateModel(id, &model))
c.ServeJSON()
}
// AddModel
// @Title AddModel
// @Tag Model API
// @Description add model
// @Param body body object.Model true "The details of the model"
// @Success 200 {object} controllers.Response The Response object
// @router /add-model [post]
func (c *ApiController) AddModel() {
var model object.Model
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddModel(&model))
c.ServeJSON()
}
// DeleteModel
// @Title DeleteModel
// @Tag Model API
// @Description delete model
// @Param body body object.Model true "The details of the model"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-model [post]
func (c *ApiController) DeleteModel() {
var model object.Model
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteModel(&model))
c.ServeJSON()
}

View File

@ -18,6 +18,8 @@ import "github.com/casdoor/casdoor/object"
// @Title GetOidcDiscovery // @Title GetOidcDiscovery
// @Tag OIDC API // @Tag OIDC API
// @Description Get Oidc Discovery
// @Success 200 {object} object.OidcDiscovery
// @router /.well-known/openid-configuration [get] // @router /.well-known/openid-configuration [get]
func (c *RootController) GetOidcDiscovery() { func (c *RootController) GetOidcDiscovery() {
host := c.Ctx.Request.Host host := c.Ctx.Request.Host
@ -27,6 +29,7 @@ func (c *RootController) GetOidcDiscovery() {
// @Title GetJwks // @Title GetJwks
// @Tag OIDC API // @Tag OIDC API
// @Success 200 {object} jose.JSONWebKey
// @router /.well-known/jwks [get] // @router /.well-known/jwks [get]
func (c *RootController) GetJwks() { func (c *RootController) GetJwks() {
jwks, err := object.GetJsonWebKeySet() jwks, err := object.GetJsonWebKeySet()

View File

@ -26,7 +26,7 @@ import (
// @Description get all records // @Description get all records
// @Param pageSize query string true "The size of each page" // @Param pageSize query string true "The size of each page"
// @Param p query string true "The number of the page" // @Param p query string true "The number of the page"
// @Success 200 {array} object.Records The Response object // @Success 200 {object} object.Record The Response object
// @router /get-records [get] // @router /get-records [get]
func (c *ApiController) GetRecords() { func (c *ApiController) GetRecords() {
limit := c.Input().Get("pageSize") limit := c.Input().Get("pageSize")
@ -50,8 +50,8 @@ func (c *ApiController) GetRecords() {
// @Tag Record API // @Tag Record API
// @Title GetRecordsByFilter // @Title GetRecordsByFilter
// @Description get records by filter // @Description get records by filter
// @Param body body object.Records true "filter Record message" // @Param filter body string true "filter Record message"
// @Success 200 {array} object.Records The Response object // @Success 200 {object} object.Record The Response object
// @router /get-records-filter [post] // @router /get-records-filter [post]
func (c *ApiController) GetRecordsByFilter() { func (c *ApiController) GetRecordsByFilter() {
body := string(c.Ctx.Input.RequestBody) body := string(c.Ctx.Input.RequestBody)

View File

@ -26,6 +26,7 @@ func (c *ApiController) GetSamlMeta() {
application := object.GetApplication(paramApp) application := object.GetApplication(paramApp)
if application == nil { if application == nil {
c.ResponseError(fmt.Sprintf("err: application %s not found", paramApp)) c.ResponseError(fmt.Sprintf("err: application %s not found", paramApp))
return
} }
metadata, _ := object.GetSamlMeta(application, host) metadata, _ := object.GetSamlMeta(application, host)
c.Data["xml"] = metadata c.Data["xml"] = metadata

View File

@ -25,33 +25,61 @@ import (
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
type EmailForm struct {
Title string `json:"title"`
Content string `json:"content"`
Sender string `json:"sender"`
Receivers []string `json:"receivers"`
Provider string `json:"provider"`
}
type SmsForm struct {
Content string `json:"content"`
Receivers []string `json:"receivers"`
OrgId string `json:"organizationId"` // e.g. "admin/built-in"
}
// SendEmail // SendEmail
// @Title SendEmail // @Title SendEmail
// @Tag Service API // @Tag Service API
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs. // @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param clientId query string true "The clientId of the application" // @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application" // @Param clientSecret query string true "The clientSecret of the application"
// @Param body body emailForm true "Details of the email request" // @Param from body controllers.EmailForm true "Details of the email request"
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /api/send-email [post] // @router /api/send-email [post]
func (c *ApiController) SendEmail() { func (c *ApiController) SendEmail() {
provider, _, ok := c.GetProviderFromContext("Email") var emailForm EmailForm
if !ok {
return
}
var emailForm struct {
Title string `json:"title"`
Content string `json:"content"`
Sender string `json:"sender"`
Receivers []string `json:"receivers"`
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm) err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
var provider *object.Provider
if emailForm.Provider != "" {
// called by frontend's TestEmailWidget, provider name is set by frontend
provider = object.GetProvider(fmt.Sprintf("admin/%s", emailForm.Provider))
} else {
// called by Casdoor SDK via Client ID & Client Secret, so the used Email provider will be the application' Email provider or the default Email provider
var ok bool
provider, _, ok = c.GetProviderFromContext("Email")
if !ok {
return
}
}
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
err := object.DailSmtpServer(provider)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk()
}
if util.IsStrsEmpty(emailForm.Title, emailForm.Content, emailForm.Sender) { if util.IsStrsEmpty(emailForm.Title, emailForm.Content, emailForm.Sender) {
c.ResponseError(fmt.Sprintf("Empty parameters for emailForm: %v", emailForm)) c.ResponseError(fmt.Sprintf("Empty parameters for emailForm: %v", emailForm))
return return
@ -86,7 +114,7 @@ func (c *ApiController) SendEmail() {
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs. // @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param clientId query string true "The clientId of the application" // @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application" // @Param clientSecret query string true "The clientSecret of the application"
// @Param body body smsForm true "Details of the sms request" // @Param from body controllers.SmsForm true "Details of the sms request"
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /api/send-sms [post] // @router /api/send-sms [post]
func (c *ApiController) SendSms() { func (c *ApiController) SendSms() {
@ -95,11 +123,7 @@ func (c *ApiController) SendSms() {
return return
} }
var smsForm struct { var smsForm SmsForm
Content string `json:"content"`
Receivers []string `json:"receivers"`
OrgId string `json:"organizationId"` // e.g. "admin/built-in"
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm) err := json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())

View File

@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -48,20 +49,28 @@ func (c *ApiController) SendVerificationCode() {
checkUser := c.Ctx.Request.Form.Get("checkUser") checkUser := c.Ctx.Request.Form.Get("checkUser")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request) remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if len(destType) == 0 || len(dest) == 0 || len(orgId) == 0 || !strings.Contains(orgId, "/") || len(checkType) == 0 || len(checkId) == 0 || len(checkKey) == 0 { if len(destType) == 0 || len(dest) == 0 || len(orgId) == 0 || !strings.Contains(orgId, "/") || len(checkType) == 0 {
c.ResponseError("Missing parameter.") c.ResponseError("Missing parameter.")
return return
} }
isHuman := false captchaProvider := captcha.GetCaptchaProvider(checkType)
captchaProvider := object.GetDefaultHumanCheckProvider()
if captchaProvider == nil {
isHuman = object.VerifyCaptcha(checkId, checkKey)
}
if !isHuman { if captchaProvider != nil {
c.ResponseError("Turing test failed.") if checkKey == "" {
return c.ResponseError("Missing parameter: checkKey.")
return
}
isHuman, err := captchaProvider.VerifyCaptcha(checkKey, checkId)
if err != nil {
c.ResponseError("Failed to verify captcha: %v", err)
return
}
if !isHuman {
c.ResponseError("Turing test failed.")
return
}
} }
user := c.getCurrentUser() user := c.getCurrentUser()
@ -173,3 +182,36 @@ func (c *ApiController) ResetEmailOrPhone() {
c.Data["json"] = Response{Status: "ok"} c.Data["json"] = Response{Status: "ok"}
c.ServeJSON() c.ServeJSON()
} }
// VerifyCaptcha ...
// @Title VerifyCaptcha
// @Tag Verification API
// @router /verify-captcha [post]
func (c *ApiController) VerifyCaptcha() {
captchaType := c.Ctx.Request.Form.Get("captchaType")
captchaToken := c.Ctx.Request.Form.Get("captchaToken")
clientSecret := c.Ctx.Request.Form.Get("clientSecret")
if captchaToken == "" {
c.ResponseError("Missing parameter: captchaToken.")
return
}
if clientSecret == "" {
c.ResponseError("Missing parameter: clientSecret.")
return
}
provider := captcha.GetCaptchaProvider(captchaType)
if provider == nil {
c.ResponseError("Invalid captcha provider.")
return
}
isValid, err := provider.VerifyCaptcha(captchaToken, clientSecret)
if err != nil {
c.ResponseError("Failed to verify captcha: %v", err)
return
}
c.ResponseOk(isValid)
}

View File

@ -38,8 +38,10 @@ func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
} }
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string { func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
hash := getMd5HexDigest(password) res := getMd5HexDigest(password)
res := getMd5HexDigest(hash + userSalt) if userSalt != "" {
res = getMd5HexDigest(res + userSalt)
}
return res return res
} }

View File

@ -38,8 +38,10 @@ func NewSha256SaltCredManager() *Sha256SaltCredManager {
} }
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string { func (cm *Sha256SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
hash := getSha256HexDigest(password) res := getSha256HexDigest(password)
res := getSha256HexDigest(hash + organizationSalt) if organizationSalt != "" {
res = getSha256HexDigest(res + organizationSalt)
}
return res return res
} }

View File

@ -25,3 +25,10 @@ func TestGetSaltedPassword(t *testing.T) {
cm := NewSha256SaltCredManager() cm := NewSha256SaltCredManager()
fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, "", salt)) fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, "", salt))
} }
func TestGetPassword(t *testing.T) {
password := "123456"
cm := NewSha256SaltCredManager()
// https://passwordsgenerator.net/sha256-hash-generator/
fmt.Printf("%s -> %s\n", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", cm.GetHashedPassword(password, "", ""))
}

View File

@ -12,8 +12,6 @@ services:
- db - db
environment: environment:
RUNNING_IN_DOCKER: "true" RUNNING_IN_DOCKER: "true"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes: volumes:
- ./conf:/conf/ - ./conf:/conf/
db: db:

View File

@ -187,9 +187,10 @@ func (idp *BilibiliIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
} }
userInfo := &UserInfo{ userInfo := &UserInfo{
Id: bUserInfoResponse.Data.OpenId, Id: bUserInfoResponse.Data.OpenId,
Username: bUserInfoResponse.Data.Name, Username: bUserInfoResponse.Data.Name,
AvatarUrl: bUserInfoResponse.Data.Face, DisplayName: bUserInfoResponse.Data.Name,
AvatarUrl: bUserInfoResponse.Data.Face,
} }
return userInfo, nil return userInfo, nil

198
idp/douyin.go Normal file
View File

@ -0,0 +1,198 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"golang.org/x/oauth2"
)
type DouyinIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewDouyinIdProvider(clientId string, clientSecret string, redirectUrl string) *DouyinIdProvider {
idp := &DouyinIdProvider{}
idp.Config = idp.getConfig(clientId, clientSecret, redirectUrl)
return idp
}
func (idp *DouyinIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *DouyinIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
TokenURL: "https://open.douyin.com/oauth/access_token",
AuthURL: "https://open.douyin.com/platform/oauth/connect",
}
var config = &oauth2.Config{
Scopes: []string{"user_info"},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-permission/get-access-token
/*
{
"data": {
"access_token": "access_token",
"description": "",
"error_code": "0",
"expires_in": "86400",
"open_id": "aaa-bbb-ccc",
"refresh_expires_in": "86400",
"refresh_token": "refresh_token",
"scope": "user_info"
},
"message": "<nil>"
}
*/
type DouyinTokenResp struct {
Data struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
OpenId string `json:"open_id"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
} `json:"data"`
Message string `json:"message"`
}
// GetToken use code to get access_token
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-permission/get-access-token
func (idp *DouyinIdProvider) GetToken(code string) (*oauth2.Token, error) {
payload := url.Values{}
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_key", idp.Config.ClientID)
payload.Set("client_secret", idp.Config.ClientSecret)
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
tokenResp := &DouyinTokenResp{}
err = json.Unmarshal(data, tokenResp)
if err != nil {
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
}
token := &oauth2.Token{
AccessToken: tokenResp.Data.AccessToken,
RefreshToken: tokenResp.Data.RefreshToken,
Expiry: time.Unix(time.Now().Unix()+tokenResp.Data.ExpiresIn, 0),
}
raw := make(map[string]interface{})
raw["open_id"] = tokenResp.Data.OpenId
token = token.WithExtra(raw)
return token, nil
}
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-management/get-account-open-info
/*
{
"data": {
"avatar": "https://example.com/x.jpeg",
"city": "上海",
"country": "中国",
"description": "",
"e_account_role": "<nil>",
"error_code": "0",
"gender": "<nil>",
"nickname": "张伟",
"open_id": "0da22181-d833-447f-995f-1beefea5bef3",
"province": "上海",
"union_id": "1ad4e099-4a0c-47d1-a410-bffb4f2f64a4"
}
}
*/
type DouyinUserInfo struct {
Data struct {
Avatar string `json:"avatar"`
City string `json:"city"`
Country string `json:"country"`
// 0->unknown, 1->male, 2->female
Gender int64 `json:"gender"`
Nickname string `json:"nickname"`
OpenId string `json:"open_id"`
Province string `json:"province"`
} `json:"data"`
}
// GetUserInfo use token to get user profile
func (idp *DouyinIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
body := &struct {
AccessToken string `json:"access_token"`
OpenId string `json:"open_id"`
}{token.AccessToken, token.Extra("open_id").(string)}
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", "https://open.douyin.com/oauth/userinfo/", bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Add("access-token", token.AccessToken)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
resp, err := idp.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var douyinUserInfo DouyinUserInfo
err = json.Unmarshal(respBody, &douyinUserInfo)
if err != nil {
return nil, err
}
userInfo := UserInfo{
Id: douyinUserInfo.Data.OpenId,
Username: douyinUserInfo.Data.Nickname,
DisplayName: douyinUserInfo.Data.Nickname,
AvatarUrl: douyinUserInfo.Data.Avatar,
}
return &userInfo, nil
}

View File

@ -86,6 +86,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl) return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Okta" { } else if typ == "Okta" {
return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl) return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Douyin" {
return NewDouyinIdProvider(clientId, clientSecret, redirectUrl)
} else if isGothSupport(typ) { } else if isGothSupport(typ) {
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl) return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
} else if typ == "Bilibili" { } else if typ == "Bilibili" {

View File

@ -138,6 +138,11 @@ func (a *Adapter) createTable() {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(Model))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Provider)) err = a.Engine.Sync2(new(Provider))
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -22,6 +22,14 @@ import (
"xorm.io/core" "xorm.io/core"
) )
type SignupItem struct {
Name string `json:"name"`
Visible bool `json:"visible"`
Required bool `json:"required"`
Prompted bool `json:"prompted"`
Rule string `json:"rule"`
}
type Application struct { type Application struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`

View File

@ -29,3 +29,16 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
return dialer.DialAndSend(message) return dialer.DialAndSend(message)
} }
// DailSmtpServer Dail Smtp server
func DailSmtpServer(provider *Provider) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
sender, err := dialer.Dial()
if err != nil {
return err
}
defer sender.Close()
return nil
}

View File

@ -23,6 +23,7 @@ import (
func InitDb() { func InitDb() {
existed := initBuiltInOrganization() existed := initBuiltInOrganization()
if !existed { if !existed {
initBuiltInProvider()
initBuiltInUser() initBuiltInUser()
initBuiltInApplication() initBuiltInApplication()
initBuiltInCert() initBuiltInCert()
@ -47,6 +48,31 @@ func initBuiltInOrganization() bool {
PhonePrefix: "86", PhonePrefix: "86",
DefaultAvatar: "https://casbin.org/img/casbin.svg", DefaultAvatar: "https://casbin.org/img/casbin.svg",
Tags: []string{}, Tags: []string{},
AccountItems: []*AccountItem{
{Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "ID", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Name", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Display name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Avatar", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "User type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Email", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Phone", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Country/Region", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Location", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Affiliation", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Homepage", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
},
} }
AddOrganization(organization) AddOrganization(organization)
return false return false
@ -102,7 +128,9 @@ func initBuiltInApplication() {
Cert: "cert-built-in", Cert: "cert-built-in",
EnablePassword: true, EnablePassword: true,
EnableSignUp: true, EnableSignUp: true,
Providers: []*ProviderItem{}, Providers: []*ProviderItem{
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Provider: nil},
},
SignupItems: []*SignupItem{ SignupItems: []*SignupItem{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"}, {Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},
{Name: "Username", Visible: true, Required: true, Prompted: false, Rule: "None"}, {Name: "Username", Visible: true, Required: true, Prompted: false, Rule: "None"},
@ -176,3 +204,20 @@ func initBuiltInLdap() {
} }
AddLdap(ldap) AddLdap(ldap)
} }
func initBuiltInProvider() {
provider := GetProvider("admin/provider_captcha_default")
if provider != nil {
return
}
provider = &Provider{
Owner: "admin",
Name: "provider_captcha_default",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Captcha Default",
Category: "Captcha",
Type: "Default",
}
AddProvider(provider)
}

122
object/model.go Normal file
View File

@ -0,0 +1,122 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Model struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
ModelText string `xorm:"mediumtext" json:"modelText"`
IsEnabled bool `json:"isEnabled"`
}
func GetModelCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Model{})
if err != nil {
panic(err)
}
return int(count)
}
func GetModels(owner string) []*Model {
models := []*Model{}
err := adapter.Engine.Desc("created_time").Find(&models, &Model{Owner: owner})
if err != nil {
panic(err)
}
return models
}
func GetPaginationModels(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Model {
models := []*Model{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&models)
if err != nil {
panic(err)
}
return models
}
func getModel(owner string, name string) *Model {
if owner == "" || name == "" {
return nil
}
model := Model{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&model)
if err != nil {
panic(err)
}
if existed {
return &model
} else {
return nil
}
}
func GetModel(id string) *Model {
owner, name := util.GetOwnerAndNameFromId(id)
return getModel(owner, name)
}
func UpdateModel(id string, model *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)
if err != nil {
panic(err)
}
return affected != 0
}
func AddModel(model *Model) bool {
affected, err := adapter.Engine.Insert(model)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteModel(model *Model) bool {
affected, err := adapter.Engine.ID(core.PK{model.Owner, model.Name}).Delete(&Model{})
if err != nil {
panic(err)
}
return affected != 0
}
func (model *Model) GetId() string {
return fmt.Sprintf("%s/%s", model.Owner, model.Name)
}

View File

@ -20,6 +20,13 @@ import (
"xorm.io/core" "xorm.io/core"
) )
type AccountItem struct {
Name string `json:"name"`
Visible bool `json:"visible"`
ViewRule string `json:"viewRule"`
ModifyRule string `json:"modifyRule"`
}
type Organization struct { type Organization struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
@ -36,6 +43,8 @@ type Organization struct {
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"` MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
EnableSoftDeletion bool `json:"enableSoftDeletion"` EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"` IsProfilePublic bool `json:"isProfilePublic"`
AccountItems []*AccountItem `xorm:"varchar(2000)" json:"accountItems"`
} }
func GetOrganizationCount(owner, field, value string) int { func GetOrganizationCount(owner, field, value string) int {
@ -121,11 +130,15 @@ func UpdateOrganization(id string, organization *Organization) bool {
} }
if name != organization.Name { if name != organization.Name {
applications := GetApplicationsByOrganizationName("admin", name) go func() {
for _, application := range applications { application := new(Application)
application.Organization = organization.Name application.Organization = organization.Name
UpdateApplication(application.GetId(), application) _, _ = adapter.Engine.Where("organization=?", name).Update(application)
}
user := new(User)
user.Owner = organization.Name
_, _ = adapter.Engine.Where("owner=?", name).Update(user)
}()
} }
if organization.MasterPassword != "" && organization.MasterPassword != "***" { if organization.MasterPassword != "" && organization.MasterPassword != "***" {

View File

@ -30,6 +30,7 @@ type Permission struct {
Users []string `xorm:"mediumtext" json:"users"` Users []string `xorm:"mediumtext" json:"users"`
Roles []string `xorm:"mediumtext" json:"roles"` Roles []string `xorm:"mediumtext" json:"roles"`
Model string `xorm:"varchar(100)" json:"model"`
ResourceType string `xorm:"varchar(100)" json:"resourceType"` ResourceType string `xorm:"varchar(100)" json:"resourceType"`
Resources []string `xorm:"mediumtext" json:"resources"` Resources []string `xorm:"mediumtext" json:"resources"`
Actions []string `xorm:"mediumtext" json:"actions"` Actions []string `xorm:"mediumtext" json:"actions"`

View File

@ -32,10 +32,10 @@ func TestProduct(t *testing.T) {
cert := getCert(product.Owner, "cert-pay-alipay") cert := getCert(product.Owner, "cert-pay-alipay")
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey) pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
paymentId := util.GenerateTimeId() paymentName := util.GenerateTimeId()
returnUrl := "" returnUrl := ""
notifyUrl := "" notifyUrl := ""
payUrl, err := pProvider.Pay(product.DisplayName, product.Name, provider.Name, paymentId, product.Price, returnUrl, notifyUrl) payUrl, err := pProvider.Pay(provider.Name, product.Name, "alice", paymentName, product.DisplayName, product.Price, returnUrl, notifyUrl)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -142,8 +142,8 @@ func GetProvider(id string) *Provider {
return getProvider(owner, name) return getProvider(owner, name)
} }
func GetDefaultHumanCheckProvider() *Provider { func GetDefaultCaptchaProvider() *Provider {
provider := Provider{Owner: "admin", Category: "HumanCheck"} provider := Provider{Owner: "admin", Category: "Captcha"}
existed, err := adapter.Engine.Get(&provider) existed, err := adapter.Engine.Get(&provider)
if err != nil { if err != nil {
panic(err) panic(err)
@ -225,3 +225,37 @@ func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
func (p *Provider) GetId() string { func (p *Provider) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name) return fmt.Sprintf("%s/%s", p.Owner, p.Name)
} }
func GetCaptchaProviderByOwnerName(applicationId string) (*Provider, error) {
owner, name := util.GetOwnerAndNameFromId(applicationId)
provider := Provider{Owner: owner, Name: name, Category: "Captcha"}
existed, err := adapter.Engine.Get(&provider)
if err != nil {
return nil, err
}
if !existed {
return nil, fmt.Errorf("the provider: %s does not exist", applicationId)
}
return &provider, nil
}
func GetCaptchaProviderByApplication(applicationId, isCurrentProvider string) (*Provider, error) {
if isCurrentProvider == "true" {
return GetCaptchaProviderByOwnerName(applicationId)
}
application := GetApplication(applicationId)
if application == nil || len(application.Providers) == 0 {
return nil, fmt.Errorf("invalid application id")
}
for _, provider := range application.Providers {
if provider.Provider == nil {
continue
}
if provider.Provider.Category == "Captcha" {
return GetCaptchaProviderByOwnerName(fmt.Sprintf("%s/%s", provider.Provider.Owner, provider.Provider.Name))
}
}
return nil, nil
}

View File

@ -51,7 +51,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
samlResponse.CreateAttr("Version", "2.0") samlResponse.CreateAttr("Version", "2.0")
samlResponse.CreateAttr("IssueInstant", now) samlResponse.CreateAttr("IssueInstant", now)
samlResponse.CreateAttr("Destination", destination) samlResponse.CreateAttr("Destination", destination)
samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("Casdoor_%s", arId)) samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId))
samlResponse.CreateElement("saml:Issuer").SetText(host) samlResponse.CreateElement("saml:Issuer").SetText(host)
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success") samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
@ -261,13 +261,15 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
} }
ctx := dsig.NewDefaultSigningContext(randomKeyStore) ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1 ctx.Hash = crypto.SHA1
signedXML, err := ctx.SignEnveloped(samlResponse) //signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
if err != nil { //if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error()) // return "", "", fmt.Errorf("err: %s", err.Error())
} //}
sig, err := ctx.ConstructSignature(samlResponse, true)
samlResponse.InsertChildAt(1, sig)
doc := etree.NewDocument() doc := etree.NewDocument()
doc.SetRoot(signedXML) doc.SetRoot(samlResponse)
xmlStr, err := doc.WriteToString() xmlStr, err := doc.WriteToString()
if err != nil { if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error()) return "", "", fmt.Errorf("err: %s", err.Error())

View File

@ -56,6 +56,9 @@ func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
// provider.Domain = "http://localhost:8000" or "https://door.casdoor.com" // provider.Domain = "http://localhost:8000" or "https://door.casdoor.com"
host = util.UrlJoin(provider.Domain, "/files") host = util.UrlJoin(provider.Domain, "/files")
} }
if provider.Type == "Azure Blob" {
host = fmt.Sprintf("%s/%s", host, provider.Bucket)
}
fileUrl := util.UrlJoin(host, objectKey) fileUrl := util.UrlJoin(host, objectKey)
if hasTimestamp { if hasTimestamp {

View File

@ -96,6 +96,7 @@ type User struct {
Steam string `xorm:"steam varchar(100)" json:"steam"` Steam string `xorm:"steam varchar(100)" json:"steam"`
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"` Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Okta string `xorm:"okta varchar(100)" json:"okta"` Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
Custom string `xorm:"custom varchar(100)" json:"custom"` Custom string `xorm:"custom varchar(100)" json:"custom"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"`

View File

@ -65,5 +65,5 @@ func RecordMessage(ctx *context.Context) {
record.Organization, record.User = util.GetOwnerAndNameFromId(userId) record.Organization, record.User = util.GetOwnerAndNameFromId(userId)
} }
util.SafeGoroutine(func() {object.AddRecord(record)}) util.SafeGoroutine(func() { object.AddRecord(record) })
} }

View File

@ -84,12 +84,19 @@ func initAPI() {
beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission") beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission")
beego.Router("/api/delete-permission", &controllers.ApiController{}, "POST:DeletePermission") beego.Router("/api/delete-permission", &controllers.ApiController{}, "POST:DeletePermission")
beego.Router("/api/get-models", &controllers.ApiController{}, "GET:GetModels")
beego.Router("/api/get-model", &controllers.ApiController{}, "GET:GetModel")
beego.Router("/api/update-model", &controllers.ApiController{}, "POST:UpdateModel")
beego.Router("/api/add-model", &controllers.ApiController{}, "POST:AddModel")
beego.Router("/api/delete-model", &controllers.ApiController{}, "POST:DeleteModel")
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone") beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone")
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode") beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone") beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-human-check", &controllers.ApiController{}, "GET:GetHumanCheck") beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha")
beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser") beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser")
beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps") beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps")

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,22 @@ paths:
tags: tags:
- OIDC API - OIDC API
operationId: RootController.GetJwks operationId: RootController.GetJwks
responses:
"200":
description: ""
schema:
$ref: '#/definitions/jose.JSONWebKey'
/.well-known/openid-configuration: /.well-known/openid-configuration:
get: get:
tags: tags:
- OIDC API - OIDC API
description: Get Oidc Discovery
operationId: RootController.GetOidcDiscovery operationId: RootController.GetOidcDiscovery
responses:
"200":
description: ""
schema:
$ref: '#/definitions/object.OidcDiscovery'
/api/add-application: /api/add-application:
post: post:
tags: tags:
@ -58,6 +69,24 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.AddLdap operationId: ApiController.AddLdap
/api/add-model:
post:
tags:
- Model API
description: add model
operationId: ApiController.AddModel
parameters:
- in: body
name: body
description: The details of the model
required: true
schema:
$ref: '#/definitions/object.Model'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-organization: /api/add-organization:
post: post:
tags: tags:
@ -243,11 +272,11 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/api/get-human-check: /api/api/get-captcha:
get: get:
tags: tags:
- Login API - Login API
operationId: ApiController.GetHumancheck operationId: ApiController.GetCaptcha
/api/api/reset-email-or-phone: /api/api/reset-email-or-phone:
post: post:
tags: tags:
@ -271,11 +300,11 @@ paths:
required: true required: true
type: string type: string
- in: body - in: body
name: body name: from
description: Details of the email request description: Details of the email request
required: true required: true
schema: schema:
$ref: '#/definitions/emailForm' $ref: '#/definitions/controllers.EmailForm'
responses: responses:
"200": "200":
description: object description: object
@ -299,11 +328,11 @@ paths:
required: true required: true
type: string type: string
- in: body - in: body
name: body name: from
description: Details of the sms request description: Details of the sms request
required: true required: true
schema: schema:
$ref: '#/definitions/smsForm' $ref: '#/definitions/controllers.SmsForm'
responses: responses:
"200": "200":
description: object description: object
@ -382,6 +411,24 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.DeleteLdap operationId: ApiController.DeleteLdap
/api/delete-model:
post:
tags:
- Model API
description: delete model
operationId: ApiController.DeleteModel
parameters:
- in: body
name: body
description: The details of the model
required: true
schema:
$ref: '#/definitions/object.Model'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-organization: /api/delete-organization:
post: post:
tags: tags:
@ -578,6 +625,43 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/get-app-login:
get:
tags:
- Login API
description: get application login
operationId: ApiController.GetApplicationLogin
parameters:
- in: query
name: clientId
description: client id
required: true
type: string
- in: query
name: responseType
description: response type
required: true
type: string
- in: query
name: redirectUri
description: redirect uri
required: true
type: string
- in: query
name: scope
description: scope
required: true
type: string
- in: query
name: state
description: state
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/Response'
/api/get-application: /api/get-application:
get: get:
tags: tags:
@ -700,6 +784,42 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.GetLdaps operationId: ApiController.GetLdaps
/api/get-model:
get:
tags:
- Model API
description: get model
operationId: ApiController.GetModel
parameters:
- in: query
name: id
description: The id of the model
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Model'
/api/get-models:
get:
tags:
- Model API
description: get models
operationId: ApiController.GetModels
parameters:
- in: query
name: owner
description: The owner of models
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Model'
/api/get-organization: /api/get-organization:
get: get:
tags: tags:
@ -901,9 +1021,7 @@ paths:
"200": "200":
description: The Response object description: The Response object
schema: schema:
type: array $ref: '#/definitions/object.Record'
items:
$ref: '#/definitions/object.Records'
/api/get-records-filter: /api/get-records-filter:
post: post:
tags: tags:
@ -912,18 +1030,17 @@ paths:
operationId: ApiController.GetRecordsByFilter operationId: ApiController.GetRecordsByFilter
parameters: parameters:
- in: body - in: body
name: body name: filter
description: filter Record message description: filter Record message
required: true required: true
schema: schema:
$ref: '#/definitions/object.Records' type: string
type: string
responses: responses:
"200": "200":
description: The Response object description: The Response object
schema: schema:
type: array $ref: '#/definitions/object.Record'
items:
$ref: '#/definitions/object.Records'
/api/get-resource: /api/get-resource:
get: get:
tags: tags:
@ -1216,6 +1333,23 @@ paths:
type: array type: array
items: items:
$ref: '#/definitions/object.Webhook' $ref: '#/definitions/object.Webhook'
/api/invoice-payment:
post:
tags:
- Payment API
description: invoice payment
operationId: ApiController.InvoicePayment
parameters:
- in: query
name: id
description: The id of the payment
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/login: /api/login:
post: post:
tags: tags:
@ -1224,21 +1358,51 @@ paths:
operationId: ApiController.Login operationId: ApiController.Login
parameters: parameters:
- in: query - in: query
name: oAuthParams name: clientId
description: oAuth parameters description: clientId
required: true required: true
type: string type: string
- in: query
name: responseType
description: responseType
required: true
type: string
- in: query
name: redirectUri
description: redirectUri
required: true
type: string
- in: query
name: scope
description: scope
type: string
- in: query
name: state
description: state
type: string
- in: query
name: nonce
description: nonce
type: string
- in: query
name: code_challenge_method
description: code_challenge_method
type: string
- in: query
name: code_challenge
description: code_challenge
type: string
- in: body - in: body
name: body name: form
description: Login information description: Login information
required: true required: true
schema: schema:
$ref: '#/definitions/RequestForm' $ref: '#/definitions/controllers.RequestForm'
responses: responses:
"200": "200":
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.api_controller.Response' $ref: '#/definitions/Response'
/api/login/oauth/access_token: /api/login/oauth/access_token:
post: post:
tags: tags:
@ -1424,6 +1588,24 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/run-syncer:
get:
tags:
- Syncer API
description: run syncer
operationId: ApiController.RunSyncer
parameters:
- in: body
name: body
description: The details of the syncer
required: true
schema:
$ref: '#/definitions/object.Syncer'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-verification-code: /api/send-verification-code:
post: post:
tags: tags:
@ -1493,42 +1675,6 @@ paths:
tags: tags:
- Login API - Login API
/api/update-application: /api/update-application:
get:
tags:
- Login API
description: get application login
operationId: ApiController.GetApplicationLogin
parameters:
- in: query
name: clientId
description: client id
required: true
type: string
- in: query
name: responseType
description: response type
required: true
type: string
- in: query
name: redirectUri
description: redirect uri
required: true
type: string
- in: query
name: scope
description: scope
required: true
type: string
- in: query
name: state
description: state
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.api_controller.Response'
post: post:
tags: tags:
- Application API - Application API
@ -1579,6 +1725,29 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.UpdateLdap operationId: ApiController.UpdateLdap
/api/update-model:
post:
tags:
- Model API
description: update model
operationId: ApiController.UpdateModel
parameters:
- in: query
name: id
description: The id of the model
required: true
type: string
- in: body
name: body
description: The details of the model
required: true
schema:
$ref: '#/definitions/object.Model'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-organization: /api/update-organization:
post: post:
tags: tags:
@ -1830,27 +1999,97 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/object.Userinfo' $ref: '#/definitions/object.Userinfo'
/api/verify-captcha:
post:
tags:
- Verification API
operationId: ApiController.VerifyCaptcha
definitions: definitions:
2127.0xc00036c600.false: 2200.0xc0003c4b70.false:
title: "false" title: "false"
type: object type: object
2161.0xc00036c630.false: 2235.0xc0003c4ba0.false:
title: "false" title: "false"
type: object type: object
RequestForm:
title: RequestForm
type: object
Response: Response:
title: Response title: Response
type: object type: object
controllers.EmailForm:
title: EmailForm
type: object
properties:
content:
type: string
receivers:
type: array
items:
type: string
sender:
type: string
title:
type: string
controllers.RequestForm:
title: RequestForm
type: object
properties:
affiliation:
type: string
application:
type: string
autoSignin:
type: boolean
code:
type: string
email:
type: string
emailCode:
type: string
firstName:
type: string
idCard:
type: string
lastName:
type: string
method:
type: string
name:
type: string
organization:
type: string
password:
type: string
phone:
type: string
phoneCode:
type: string
phonePrefix:
type: string
provider:
type: string
redirectUri:
type: string
region:
type: string
relayState:
type: string
samlRequest:
type: string
samlResponse:
type: string
state:
type: string
type:
type: string
username:
type: string
controllers.Response: controllers.Response:
title: Response title: Response
type: object type: object
properties: properties:
data: data:
$ref: '#/definitions/2127.0xc00036c600.false' $ref: '#/definitions/2200.0xc0003c4b70.false'
data2: data2:
$ref: '#/definitions/2161.0xc00036c630.false' $ref: '#/definitions/2235.0xc0003c4ba0.false'
msg: msg:
type: string type: string
name: name:
@ -1859,25 +2098,33 @@ definitions:
type: string type: string
sub: sub:
type: string type: string
controllers.api_controller.Response: controllers.SmsForm:
title: Response title: SmsForm
type: object type: object
properties: properties:
data: content:
$ref: '#/definitions/2127.0xc00036c600.false' type: string
data2: organizationId:
$ref: '#/definitions/2161.0xc00036c630.false' type: string
msg: receivers:
type: array
items:
type: string
jose.JSONWebKey:
title: JSONWebKey
type: object
object.AccountItem:
title: AccountItem
type: object
properties:
modifyRule:
type: string type: string
name: name:
type: string type: string
status: viewRule:
type: string type: string
sub: visible:
type: string type: boolean
emailForm:
title: emailForm
type: object
object.Adapter: object.Adapter:
title: Adapter title: Adapter
type: object type: object
@ -2037,10 +2284,80 @@ definitions:
type: string type: string
username: username:
type: string type: string
object.Model:
title: Model
type: object
properties:
createdTime:
type: string
displayName:
type: string
isEnabled:
type: boolean
modelText:
type: string
name:
type: string
owner:
type: string
object.OidcDiscovery:
title: OidcDiscovery
type: object
properties:
authorization_endpoint:
type: string
claims_supported:
type: array
items:
type: string
grant_types_supported:
type: array
items:
type: string
id_token_signing_alg_values_supported:
type: array
items:
type: string
introspection_endpoint:
type: string
issuer:
type: string
jwks_uri:
type: string
request_object_signing_alg_values_supported:
type: array
items:
type: string
request_parameter_supported:
type: boolean
response_modes_supported:
type: array
items:
type: string
response_types_supported:
type: array
items:
type: string
scopes_supported:
type: array
items:
type: string
subject_types_supported:
type: array
items:
type: string
token_endpoint:
type: string
userinfo_endpoint:
type: string
object.Organization: object.Organization:
title: Organization title: Organization
type: object type: object
properties: properties:
accountItems:
type: array
items:
$ref: '#/definitions/object.AccountItem'
createdTime: createdTime:
type: string type: string
defaultAvatar: defaultAvatar:
@ -2051,6 +2368,8 @@ definitions:
type: boolean type: boolean
favicon: favicon:
type: string type: string
isProfilePublic:
type: boolean
masterPassword: masterPassword:
type: string type: string
name: name:
@ -2081,6 +2400,16 @@ definitions:
type: string type: string
displayName: displayName:
type: string type: string
invoiceRemark:
type: string
invoiceTaxId:
type: string
invoiceTitle:
type: string
invoiceType:
type: string
invoiceUrl:
type: string
message: message:
type: string type: string
name: name:
@ -2091,6 +2420,14 @@ definitions:
type: string type: string
payUrl: payUrl:
type: string type: string
personEmail:
type: string
personIdCard:
type: string
personName:
type: string
personPhone:
type: string
price: price:
type: number type: number
format: double format: double
@ -2126,6 +2463,8 @@ definitions:
type: string type: string
isEnabled: isEnabled:
type: boolean type: boolean
model:
type: string
name: name:
type: string type: string
owner: owner:
@ -2205,6 +2544,16 @@ definitions:
type: string type: string
createdTime: createdTime:
type: string type: string
customAuthUrl:
type: string
customLogo:
type: string
customScope:
type: string
customTokenUrl:
type: string
customUserInfoUrl:
type: string
displayName: displayName:
type: string type: string
domain: domain:
@ -2264,9 +2613,35 @@ definitions:
type: boolean type: boolean
provider: provider:
$ref: '#/definitions/object.Provider' $ref: '#/definitions/object.Provider'
object.Records: object.Record:
title: Records title: Record
type: object type: object
properties:
action:
type: string
clientIp:
type: string
createdTime:
type: string
extendedUser:
$ref: '#/definitions/object.User'
id:
type: integer
format: int64
isTriggered:
type: boolean
method:
type: string
name:
type: string
organization:
type: string
owner:
type: string
requestUri:
type: string
user:
type: string
object.Role: object.Role:
title: Role title: Role
type: object type: object
@ -2442,6 +2817,8 @@ definitions:
type: string type: string
baidu: baidu:
type: string type: string
bilibili:
type: string
bio: bio:
type: string type: string
birthday: birthday:
@ -2452,10 +2829,14 @@ definitions:
type: string type: string
createdTime: createdTime:
type: string type: string
custom:
type: string
dingtalk: dingtalk:
type: string type: string
displayName: displayName:
type: string type: string
douyin:
type: string
education: education:
type: string type: string
email: email:
@ -2521,6 +2902,8 @@ definitions:
type: string type: string
name: name:
type: string type: string
okta:
type: string
owner: owner:
type: string type: string
password: password:
@ -2558,6 +2941,8 @@ definitions:
type: string type: string
type: type:
type: string type: string
unionId:
type: string
updatedTime: updatedTime:
type: string type: string
wechat: wechat:
@ -2618,9 +3003,6 @@ definitions:
type: string type: string
url: url:
type: string type: string
smsForm:
title: smsForm
type: object
xorm.Engine: xorm.Engine:
title: Engine title: Engine
type: object type: object

View File

@ -1,13 +1,23 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?5998fcd123c220efc0936edf4f250504";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<!-- <link rel="icon" href="%PUBLIC_URL%/favicon.png" />--> <!-- <link rel="icon" href="%PUBLIC_URL%/favicon.png" />-->
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" 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.casdoor.com/static/favicon.png" />
<!-- <!--

242
web/src/AccountTable.js Normal file
View File

@ -0,0 +1,242 @@
// 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 {DownOutlined, DeleteOutlined, UpOutlined} from '@ant-design/icons';
import {Button, Col, Row, Select, Switch, Table, Tooltip} from 'antd';
import * as Setting from "./Setting";
import i18next from "i18next";
const { Option } = Select;
class AccountTable 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) {
let row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true};
if (table === undefined) {
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("provider:Name"),
dataIndex: 'name',
key: 'name',
render: (text, record, index) => {
const items = [
{name: "Organization", displayName: i18next.t("general:Organization")},
{name: "ID", displayName: i18next.t("general:ID")},
{name: "Name", displayName: i18next.t("general:Name")},
{name: "Display name", displayName: i18next.t("general:Display name")},
{name: "Avatar", displayName: i18next.t("general:Avatar")},
{name: "User type", displayName: i18next.t("general:User type")},
{name: "Password", displayName: i18next.t("general:Password")},
{name: "Email", displayName: i18next.t("general:Email")},
{name: "Phone", displayName: i18next.t("general:Phone")},
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{name: "Location", displayName: i18next.t("user:Location")},
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},
{name: "Title", displayName: i18next.t("user:Title")},
{name: "Homepage", displayName: i18next.t("user:Homepage")},
{name: "Bio", displayName: i18next.t("user:Bio")},
{name: "Tag", displayName: i18next.t("user:Tag")},
{name: "Signup application", displayName: i18next.t("general:Signup application")},
{name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")},
{name: "Properties", displayName: i18next.t("user:Properties")},
{name: "Is admin", displayName: i18next.t("user:Is admin")},
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
];
const getItemDisplayName = (text) => {
const item = items.filter(item => item.name === text);
if (item.length === 0) {
return "";
}
return item[0].displayName;
};
return (
<Select virtual={false} style={{width: '100%'}}
value={getItemDisplayName(text)}
onChange={value => {
this.updateField(table, index, 'name', value);
}} >
{
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>)
}
</Select>
)
}
},
{
title: i18next.t("provider:visible"),
dataIndex: 'visible',
key: 'visible',
width: '120px',
render: (text, record, index) => {
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, 'visible', checked);
}} />
)
}
},
{
title: i18next.t("organization:viewRule"),
dataIndex: 'viewRule',
key: 'viewRule',
width: '155px',
render: (text, record, index) => {
if (!record.visible) {
return null;
}
let options = [
{id: 'Public', name: 'Public'},
{id: 'Self', name: 'Self'},
{id: 'Admin', name: 'Admin'},
];
return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {
this.updateField(table, index, 'viewRule', value);
})}>
{
options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
)
}
},
{
title: i18next.t("organization:modifyRule"),
dataIndex: 'modifyRule',
key: 'modifyRule',
width: '155px',
render: (text, record, index) => {
if (!record.visible) {
return null;
}
let options;
if (record.viewRule === "Admin") {
options = [
{id: 'Admin', name: 'Admin'},
{id: 'Immutable', name: 'Immutable'},
];
} else {
options = [
{id: 'Self', name: 'Self'},
{id: 'Admin', name: 'Admin'},
{id: 'Immutable', name: 'Immutable'},
];
}
return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {
this.updateField(table, index, 'modifyRule', value);
})}>
{
options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
)
}
},
{
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 AccountTable;

View File

@ -69,6 +69,8 @@ import PromptPage from "./auth/PromptPage";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage"; import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from './auth/SamlCallback'; import SamlCallback from './auth/SamlCallback';
import CasLogout from "./auth/CasLogout"; import CasLogout from "./auth/CasLogout";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
const { Header, Footer } = Layout; const { Header, Footer } = Layout;
@ -118,6 +120,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/roles' }); this.setState({ selectedMenuKey: '/roles' });
} else if (uri.includes('/permissions')) { } else if (uri.includes('/permissions')) {
this.setState({ selectedMenuKey: '/permissions' }); this.setState({ selectedMenuKey: '/permissions' });
} else if (uri.includes('/models')) {
this.setState({ selectedMenuKey: '/models' });
} else if (uri.includes('/providers')) { } else if (uri.includes('/providers')) {
this.setState({ selectedMenuKey: '/providers' }); this.setState({ selectedMenuKey: '/providers' });
} else if (uri.includes('/applications')) { } else if (uri.includes('/applications')) {
@ -382,6 +386,13 @@ class App extends Component {
</Link> </Link>
</Menu.Item> </Menu.Item>
); );
res.push(
<Menu.Item key="/models">
<Link to="/models">
{i18next.t("general:Models")}
</Link>
</Menu.Item>
);
res.push( res.push(
<Menu.Item key="/providers"> <Menu.Item key="/providers">
<Link to="/providers"> <Link to="/providers">
@ -514,6 +525,8 @@ class App extends Component {
<Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)}/> <Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)}/> <Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)}/>
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)}/> <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="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage 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="/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} />)}/> <Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from 'antd'; import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from 'antd';
import {LinkOutlined, UploadOutlined} from "@ant-design/icons"; import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -28,14 +28,15 @@ import UrlTable from "./UrlTable";
import ProviderTable from "./ProviderTable"; import ProviderTable from "./ProviderTable";
import SignupTable from "./SignupTable"; import SignupTable from "./SignupTable";
import PromptPage from "./auth/PromptPage"; import PromptPage from "./auth/PromptPage";
import copy from "copy-to-clipboard";
import {Controlled as CodeMirror} from 'react-codemirror2'; import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
require('codemirror/theme/material-darker.css'); require('codemirror/theme/material-darker.css');
require("codemirror/mode/htmlmixed/htmlmixed"); require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml");
const { Option } = Select; const { Option } = Select;
const { TextArea } = Input;
class ApplicationEditPage extends React.Component { class ApplicationEditPage extends React.Component {
constructor(props) { constructor(props) {
@ -180,7 +181,7 @@ class ApplicationEditPage extends React.Component {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} : {Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} :
</Col> </Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}> <Col span={22} style={(Setting.isMobile()) ? {maxWidth: '100%'} :{}}>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
@ -402,7 +403,7 @@ class ApplicationEditPage extends React.Component {
}}/> }}/>
<Upload maxCount={1} accept=".html" showUploadList={false} <Upload maxCount={1} accept=".html" showUploadList={false}
beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}> beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}>
<Button icon={<UploadOutlined />} loading={this.state.uploading}>Click to Upload</Button> <Button icon={<UploadOutlined />} loading={this.state.uploading}>{i18next.t("general:Click to Upload")}</Button>
</Upload> </Upload>
</Col> </Col>
</Row> </Row>
@ -478,7 +479,11 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} : {Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
</Col> </Col>
<Col span={22}> <Col span={22}>
<TextArea rows={8} value={this.state.samlMetadata} /> <CodeMirror
value={this.state.samlMetadata}
options={{mode: 'xml', theme: 'default'}}
onBeforeChange={(editor, data, value) => {}}
/>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
@ -500,7 +505,7 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col> </Col>
{ {
this.renderPreview() this.renderSignupSigninPreview()
} }
</Row> </Row>
{ {
@ -524,29 +529,33 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col> </Col>
{ {
this.renderPreview2() this.renderPromptPreview()
} }
</Row> </Row>
</Card> </Card>
) )
} }
renderPreview() { renderSignupSigninPreview() {
let signUpUrl = `/signup/${this.state.application.name}`; let signUpUrl = `/signup/${this.state.application.name}`;
let signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`; let signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`;
let maskStyle = {position: 'absolute', top: '0px', left: '0px', zIndex: 10, height: '100%', width: '100%', background: 'rgba(0,0,0,0.4)'};
if (!this.state.application.enablePassword) { if (!this.state.application.enablePassword) {
signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize"); signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize");
} }
if (!Setting.isMobile()) {
return ( return (
<React.Fragment> <React.Fragment>
<Col span={11} style={{display:"flex", flexDirection: "column"}}> <Col span={11}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}> <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<Button type="primary">{i18next.t("application:Test signup page..")}</Button> copy(`${window.location.origin}${signUpUrl}`);
</a> Setting.showMessage("success", i18next.t("application:Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
>
{i18next.t("application:Copy signup page URL")}
</Button>
<br/> <br/>
<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"}}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
{ {
this.state.application.enablePassword ? ( this.state.application.enablePassword ? (
<SignupPage application={this.state.application} /> <SignupPage application={this.state.application} />
@ -554,64 +563,45 @@ class ApplicationEditPage extends React.Component {
<LoginPage type={"login"} mode={"signup"} application={this.state.application} /> <LoginPage type={"login"} mode={"signup"} application={this.state.application} />
) )
} }
<div style={maskStyle}></div>
</div> </div>
</Col> </Col>
<Col span={11} style={{display:"flex", flexDirection: "column"}}> <Col span={11}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}> <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<Button type="primary">{i18next.t("application:Test signin page..")}</Button> copy(`${window.location.origin}${signInUrl}`);
</a> Setting.showMessage("success", i18next.t("application:Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
>
{i18next.t("application:Copy signin page URL")}
</Button>
<br/> <br/>
<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"}}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
</div>
</Col>
</React.Fragment>
)
} else{
return(
<React.Fragment>
<Col span={24} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Button type="primary">{i18next.t("application:Test signup page..")}</Button>
</a>
<div style={{marginBottom:"10px", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
) : (
<LoginPage type={"login"} mode={"signup"} application={this.state.application} />
)
}
</div>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}>
<Button type="primary">{i18next.t("application:Test signin page..")}</Button>
</a>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} /> <LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle}></div>
</div> </div>
</Col> </Col>
</React.Fragment> </React.Fragment>
) )
} }
}
renderPreview2() { renderPromptPreview() {
let promptUrl = `/prompt/${this.state.application.name}`; let promptUrl = `/prompt/${this.state.application.name}`;
let maskStyle = {position: 'absolute', top: '0px', left: '0px', zIndex: 10, height: '100%', width: '100%', background: 'rgba(0,0,0,0.4)'};
return ( return (
<React.Fragment> <Col span={11}>
<Col span={(Setting.isMobile()) ? 24 : 11} style={{display:"flex", flexDirection: "column", flex: "auto"}} > <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<a style={{marginBottom: "10px"}} target="_blank" rel="noreferrer" href={promptUrl}> copy(`${window.location.origin}${promptUrl}`);
<Button type="primary">{i18next.t("application:Test prompt page..")}</Button> Setting.showMessage("success", i18next.t("application:Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
</a> }}
<br style={(Setting.isMobile()) ? {display: "none"} : {}} /> >
<br style={(Setting.isMobile()) ? {display: "none"} : {}} /> {i18next.t("application:Copy prompt page URL")}
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}> </Button>
<PromptPage application={this.state.application} account={this.props.account} /> <br/>
</div> <div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
</Col> <PromptPage application={this.state.application} account={this.props.account} />
</React.Fragment> <div style={maskStyle}></div>
</div>
</Col>
) )
} }

View File

@ -23,7 +23,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class ApplicationListPage extends BaseListPage { class ApplicationListPage extends BaseListPage {
newApplication() { newApplication() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {
@ -36,7 +35,9 @@ class ApplicationListPage extends BaseListPage {
enableSignUp: true, enableSignUp: true,
enableSigninSession: false, enableSigninSession: false,
enableCodeSignin: false, enableCodeSignin: false,
providers: [], providers: [
{name: "provider_captcha_default", canSignUp: false, canSignIn: false, canUnlink: false, prompted: false, alertType: "None"},
],
signupItems: [ signupItems: [
{name: "ID", visible: false, required: true, rule: "Random"}, {name: "ID", visible: false, required: true, rule: "Random"},
{name: "Username", visible: true, required: true, rule: "None"}, {name: "Username", visible: true, required: true, rule: "None"},

View File

@ -22,7 +22,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class CertListPage extends BaseListPage { class CertListPage extends BaseListPage {
newCert() { newCert() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {

208
web/src/ModelEditPage.js Normal file
View File

@ -0,0 +1,208 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
import * as ModelBackend from "./backend/ModelBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import TextArea from "antd/es/input/TextArea";
const { Option } = Select;
class ModelEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
modelName: props.match.params.modelName,
model: null,
organizations: [],
users: [],
models: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getModel();
this.getOrganizations();
}
getModel() {
ModelBackend.getModel(this.state.organizationName, this.state.modelName)
.then((model) => {
this.setState({
model: model,
});
this.getModels(model.owner);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getModels(organizationName) {
ModelBackend.getModels(organizationName)
.then((res) => {
this.setState({
models: res,
});
});
}
parseModelField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateModelField(key, value) {
value = this.parseModelField(key, value);
let model = this.state.model;
model[key] = value;
this.setState({
model: model,
});
}
renderModel() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("model:New Model") : i18next.t("model:Edit Model")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteModel()}>{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.model.owner} onChange={(value => {this.updateModelField('owner', 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.model.name} onChange={e => {
this.updateModelField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.model.displayName} onChange={e => {
this.updateModelField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("model:Model text"), i18next.t("model:Model text - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={10} value={this.state.model.modelText} onChange={e => {
this.updateModelField('modelText', e.target.value);
}} />
</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.model.isEnabled} onChange={checked => {
this.updateModelField('isEnabled', checked);
}} />
</Col>
</Row>
</Card>
)
}
submitModelEdit(willExist) {
let model = Setting.deepCopy(this.state.model);
ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
modelName: this.state.model.name,
});
if (willExist) {
this.props.history.push(`/models`);
} else {
this.props.history.push(`/models/${this.state.model.owner}/${this.state.model.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateModelField('name', this.state.modelName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
deleteModel() {
ModelBackend.deleteModel(this.state.model)
.then(() => {
this.props.history.push(`/models`);
})
.catch(error => {
Setting.showMessage("error", `Model failed to delete: ${error}`);
});
}
render() {
return (
<div>
{
this.state.model !== null ? this.renderModel() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteModel()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default ModelEditPage;

201
web/src/ModelListPage.js Normal file
View File

@ -0,0 +1,201 @@
// Copyright 2021 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 ModelBackend from "./backend/ModelBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ModelListPage extends BaseListPage {
newModel() {
const randomName = Setting.getRandomName();
return {
owner: "built-in",
name: `model_${randomName}`,
createdTime: moment().format(),
displayName: `New Model - ${randomName}`,
modelText: "",
isEnabled: true,
}
}
addModel() {
const newModel = this.newModel();
ModelBackend.addModel(newModel)
.then((res) => {
this.props.history.push({pathname: `/models/${newModel.owner}/${newModel.name}`, mode: "add"});
}
)
.catch(error => {
Setting.showMessage("error", `Model failed to add: ${error}`);
});
}
deleteModel(i) {
ModelBackend.deleteModel(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Model deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Model failed to delete: ${error}`);
});
}
renderTable(models) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: 'owner',
key: 'owner',
width: '120px',
sorter: true,
...this.getColumnSearchProps('owner'),
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={`/models/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
width: '200px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
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(`/models/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete model: ${record.name} ?`}
onConfirm={() => this.deleteModel(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={models} rowKey="name" size="middle" bordered
pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Models")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small"
onClick={this.addModel.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
ModelBackend.getModels("", 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 ModelListPage;

View File

@ -20,6 +20,7 @@ import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons"; import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./LdapTable"; import LdapTable from "./LdapTable";
import AccountTable from "./AccountTable";
const { Option } = Select; const { Option } = Select;
@ -117,7 +118,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
@ -127,7 +128,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
@ -187,7 +188,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
@ -197,7 +198,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
@ -250,6 +251,18 @@ class OrganizationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
</Col>
<Col span={22} >
<AccountTable
title={i18next.t("organization:Account items")}
table={this.state.organization.accountItems}
onUpdateTable={(value) => { this.updateOrganizationField('accountItems', value)}}
/>
</Col>
</Row>
<Row style={{marginTop: '20px'}}> <Row style={{marginTop: '20px'}}>
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} : {Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :

View File

@ -22,7 +22,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class OrganizationListPage extends BaseListPage { class OrganizationListPage extends BaseListPage {
newOrganization() { newOrganization() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {
@ -40,6 +39,31 @@ class OrganizationListPage extends BaseListPage {
masterPassword: "", masterPassword: "",
enableSoftDeletion: false, enableSoftDeletion: false,
isProfilePublic: true, isProfilePublic: true,
accountItems: [
{name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Name", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Display name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Avatar", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "User type", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Password", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Email", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Phone", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Country/Region", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Location", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Affiliation", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Title", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Homepage", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is global admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"},
],
} }
} }

View File

@ -20,6 +20,7 @@ import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend"; import * as RoleBackend from "./backend/RoleBackend";
import * as ModelBackend from "./backend/ModelBackend";
const { Option } = Select; const { Option } = Select;
@ -34,6 +35,7 @@ class PermissionEditPage extends React.Component {
organizations: [], organizations: [],
users: [], users: [],
roles: [], roles: [],
models: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
} }
@ -52,6 +54,7 @@ class PermissionEditPage extends React.Component {
this.getUsers(permission.owner); this.getUsers(permission.owner);
this.getRoles(permission.owner); this.getRoles(permission.owner);
this.getModels(permission.owner);
}); });
} }
@ -82,6 +85,15 @@ class PermissionEditPage extends React.Component {
}); });
} }
getModels(organizationName) {
ModelBackend.getModels(organizationName)
.then((res) => {
this.setState({
models: res,
});
});
}
parsePermissionField(key, value) { parsePermissionField(key, value) {
if ([""].includes(key)) { if ([""].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
@ -146,6 +158,20 @@ class PermissionEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </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.permission.model} onChange={(model => {
this.updatePermissionField('model', model);
})}>
{
this.state.models.map((model, index) => <Option key={index} value={model.name}>{model.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} : {Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :

View File

@ -206,7 +206,7 @@ class ProductBuyPage extends React.Component {
<Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item> <Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item> <Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Image")} span={3}> <Descriptions.Item label={i18next.t("product:Image")} span={3}>
<img src={product?.image} alt={product?.image} height={90} style={{marginBottom: '20px'}}/> <img src={product?.image} alt={product?.name} height={90} style={{marginBottom: '20px'}}/>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")}> <Descriptions.Item label={i18next.t("product:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}> <span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>

View File

@ -110,7 +110,7 @@ class ProductEditPage extends React.Component {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} : {Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
</Col> </Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}> <Col span={22} style={(Setting.isMobile()) ? {maxWidth: '100%'} :{}}>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :

View File

@ -19,7 +19,9 @@ import * as ProviderBackend from "./backend/ProviderBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import { authConfig } from "./auth/Auth"; import { authConfig } from "./auth/Auth";
import * as ProviderEditTestEmail from "./TestEmailWidget";
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { CaptchaPreview } from "./common/CaptchaPreview";
const { Option } = Select; const { Option } = Select;
const { TextArea } = Input; const { TextArea } = Input;
@ -32,6 +34,7 @@ class ProviderEditPage extends React.Component {
providerName: props.match.params.providerName, providerName: props.match.params.providerName,
provider: null, provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
testEmail: this.props.account["email"] !== undefined ? this.props.account["email"] : "",
}; };
} }
@ -70,10 +73,15 @@ class ProviderEditPage extends React.Component {
case "Email": case "Email":
return Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip")); return Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"));
case "SMS": case "SMS":
if (this.state.provider.type === "Volc Engine SMS") if (this.state.provider.type === "Volc Engine SMS") {
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip")); return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
if (this.state.provider.type === "Huawei Cloud SMS") } else if (this.state.provider.type === "Huawei Cloud SMS") {
return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip")); return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
case "Captcha":
return Setting.getLabel(i18next.t("provider:Site key"), i18next.t("provider:Site key - Tooltip"));
default: default:
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip")); return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
} }
@ -84,10 +92,15 @@ class ProviderEditPage extends React.Component {
case "Email": case "Email":
return Setting.getLabel(i18next.t("login:Password"), i18next.t("login:Password - Tooltip")); return Setting.getLabel(i18next.t("login:Password"), i18next.t("login:Password - Tooltip"));
case "SMS": case "SMS":
if (this.state.provider.type === "Volc Engine SMS") if (this.state.provider.type === "Volc Engine SMS") {
return Setting.getLabel(i18next.t("provider:Secret access key"), i18next.t("provider:SecretAccessKey - Tooltip")); return Setting.getLabel(i18next.t("provider:Secret access key"), i18next.t("provider:SecretAccessKey - Tooltip"));
if (this.state.provider.type === "Huawei Cloud SMS") } else if (this.state.provider.type === "Huawei Cloud SMS") {
return Setting.getLabel(i18next.t("provider:App secret"), i18next.t("provider:AppSecret - Tooltip")); return Setting.getLabel(i18next.t("provider:App secret"), i18next.t("provider:AppSecret - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
case "Captcha":
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
default: default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
} }
@ -187,6 +200,8 @@ class ProviderEditPage extends React.Component {
this.updateProviderField('domain', Setting.getFullServerUrl()); this.updateProviderField('domain', Setting.getFullServerUrl());
} else if (value === "SAML") { } else if (value === "SAML") {
this.updateProviderField('type', 'Aliyun IDaaS'); this.updateProviderField('type', 'Aliyun IDaaS');
} else if (value === "Captcha") {
this.updateProviderField('type', 'Default');
} }
})}> })}>
{ {
@ -197,6 +212,7 @@ class ProviderEditPage extends React.Component {
{id: 'Storage', name: 'Storage'}, {id: 'Storage', name: 'Storage'},
{id: 'SAML', name: 'SAML'}, {id: 'SAML', name: 'SAML'},
{id: 'Payment', name: 'Payment'}, {id: 'Payment', name: 'Payment'},
{id: 'Captcha', name: 'Captcha'},
].map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>) ].map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
} }
</Select> </Select>
@ -243,7 +259,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
{ {
this.state.provider.type !== "WeCom" ? null : ( this.state.provider.type !== "WeCom" ? null : (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}> <Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Method"), i18next.t("provider:Method - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Method"), i18next.t("provider:Method - Tooltip"))} :
@ -311,7 +327,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
@ -321,7 +337,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
@ -335,26 +351,32 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) )
} }
<Row style={{marginTop: '20px'}} > {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> this.state.provider.category === "Captcha" && this.state.provider.type === "Default" ? null : (
{this.getClientIdLabel()} <React.Fragment>
</Col> <Row style={{marginTop: '20px'}} >
<Col span={22} > <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
<Input value={this.state.provider.clientId} onChange={e => { {this.getClientIdLabel()}
this.updateProviderField('clientId', e.target.value); </Col>
}} /> <Col span={22} >
</Col> <Input value={this.state.provider.clientId} onChange={e => {
</Row> this.updateProviderField('clientId', e.target.value);
<Row style={{marginTop: '20px'}} > }} />
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> </Col>
{this.getClientSecretLabel()} </Row>
</Col> <Row style={{marginTop: '20px'}} >
<Col span={22} > <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
<Input value={this.state.provider.clientSecret} onChange={e => { {this.getClientSecretLabel()}
this.updateProviderField('clientSecret', e.target.value); </Col>
}} /> <Col span={22} >
</Col> <Input value={this.state.provider.clientSecret} onChange={e => {
</Row> this.updateProviderField('clientSecret', e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
)
}
{ {
this.state.provider.type !== "WeChat" ? null : ( this.state.provider.type !== "WeChat" ? null : (
<React.Fragment> <React.Fragment>
@ -494,6 +516,27 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{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})
}} />
</Col>
<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)} >
{i18next.t("provider:Send Test Email")}
</Button>
</Row>
</React.Fragment> </React.Fragment>
) : this.state.provider.category === "SMS" ? ( ) : this.state.provider.category === "SMS" ? (
<React.Fragment> <React.Fragment>
@ -631,6 +674,27 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
{
this.state.provider.category !== "Captcha" ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
<Col span={22} >
<CaptchaPreview
provider={this.state.provider}
providerName={this.state.providerName}
clientSecret={this.state.provider.clientSecret}
captchaType={this.state.provider.type}
owner={this.state.provider.owner}
clientId={this.state.provider.clientId}
name={this.state.provider.name}
providerUrl={this.state.provider.providerUrl}
/>
</Col>
</Row>
)
}
</Card> </Card>
) )
} }

View File

@ -23,7 +23,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class ProviderListPage extends BaseListPage { class ProviderListPage extends BaseListPage {
newProvider() { newProvider() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {
@ -134,6 +133,7 @@ class ProviderListPage extends BaseListPage {
{text: 'SMS', value: 'SMS', children: Setting.getProviderTypeOptions('SMS').map((o) => {return {text:o.id, value:o.name}})}, {text: 'SMS', value: 'SMS', children: Setting.getProviderTypeOptions('SMS').map((o) => {return {text:o.id, value:o.name}})},
{text: 'Storage', value: 'Storage', children: Setting.getProviderTypeOptions('Storage').map((o) => {return {text:o.id, value:o.name}})}, {text: 'Storage', value: 'Storage', children: Setting.getProviderTypeOptions('Storage').map((o) => {return {text:o.id, value:o.name}})},
{text: 'SAML', value: 'SAML', children: Setting.getProviderTypeOptions('SAML').map((o) => {return {text:o.id, value:o.name}})}, {text: 'SAML', value: 'SAML', children: Setting.getProviderTypeOptions('SAML').map((o) => {return {text:o.id, value:o.name}})},
{text: 'Captcha', value: 'Captcha', children: Setting.getProviderTypeOptions('Captcha').map((o) => {return {text:o.id, value:o.name}})},
], ],
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {

View File

@ -22,7 +22,6 @@ import moment from "moment";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class RecordListPage extends BaseListPage { class RecordListPage extends BaseListPage {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.state.pagination.pageSize = 20; this.state.pagination.pageSize = 20;
const { pagination } = this.state; const { pagination } = this.state;

View File

@ -206,7 +206,7 @@ class ResourceListPage extends BaseListPage {
render: (text, record, index) => { render: (text, record, index) => {
if (record.fileType === "image") { if (record.fileType === "image") {
return ( return (
<a target="_blank" href={record.url}> <a target="_blank" rel="noreferrer" href={record.url}>
<img src={record.url} alt={record.name} width={100} /> <img src={record.url} alt={record.name} width={100} />
</a> </a>
) )

View File

@ -21,7 +21,6 @@ import i18next from "i18next";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth"; import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import moment from "moment";
import * as Conf from "./Conf"; import * as Conf from "./Conf";
export let ServerUrl = ""; export let ServerUrl = "";
@ -107,6 +106,20 @@ export const OtherProviderInfo = {
url: "https://gc.org" url: "https://gc.org"
}, },
}, },
Captcha: {
"Default": {
logo: `${StaticBaseUrl}/img/social_default.png`,
url: "https://pkg.go.dev/github.com/dchest/captcha",
},
"reCAPTCHA": {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha",
},
"hCaptcha": {
logo: `${StaticBaseUrl}/img/social_hcaptcha.png`,
url: "https://www.hcaptcha.com",
}
}
}; };
export function getCountryRegionData() { export function getCountryRegionData() {
@ -225,7 +238,7 @@ export function isValidInvoiceTitle(invoiceTitle) {
} }
// https://blog.css8.cn/post/14210975.html // https://blog.css8.cn/post/14210975.html
const invoiceTitleRegex = /^[\(\)\\\u4e00-\u9fa5]{0,50}$/; const invoiceTitleRegex = /^[()\u4e00-\u9fa5]{0,50}$/;
return invoiceTitleRegex.test(invoiceTitle); return invoiceTitleRegex.test(invoiceTitle);
} }
@ -474,27 +487,26 @@ export function changeLanguage(language) {
} }
export function changeMomentLanguage(language) { export function changeMomentLanguage(language) {
return; // if (language === "zh") {
if (language === "zh") { // moment.locale("zh", {
moment.locale("zh", { // relativeTime: {
relativeTime: { // future: "%s内",
future: "%s内", // past: "%s前",
past: "%s前", // s: "几秒",
s: "几秒", // ss: "%d秒",
ss: "%d秒", // m: "1分钟",
m: "1分钟", // mm: "%d分钟",
mm: "%d分钟", // h: "1小时",
h: "1小时", // hh: "%d小时",
hh: "%d小时", // d: "1天",
d: "1天", // dd: "%d天",
dd: "%d天", // M: "1个月",
M: "1个月", // MM: "%d个月",
MM: "%d个月", // y: "1年",
y: "1年", // yy: "%d年",
yy: "%d年", // },
}, // });
}); // }
}
} }
export function getClickable(text) { export function getClickable(text) {
@ -556,6 +568,7 @@ export function getProviderTypeOptions(category) {
{id: 'Steam', name: 'Steam'}, {id: 'Steam', name: 'Steam'},
{id: 'Bilibili', name: 'Bilibili'}, {id: 'Bilibili', name: 'Bilibili'},
{id: 'Okta', name: 'Okta'}, {id: 'Okta', name: 'Okta'},
{id: 'Douyin', name: 'Douyin'},
{id: 'Custom', name: 'Custom'}, {id: 'Custom', name: 'Custom'},
] ]
); );
@ -596,6 +609,12 @@ export function getProviderTypeOptions(category) {
{id: 'PayPal', name: 'PayPal'}, {id: 'PayPal', name: 'PayPal'},
{id: 'GC', name: 'GC'}, {id: 'GC', name: 'GC'},
]); ]);
} else if (category === "Captcha") {
return ([
{id: 'Default', name: 'Default'},
{id: 'reCAPTCHA', name: 'reCAPTCHA'},
{id: 'hCaptcha', name: 'hCaptcha'},
]);
} else { } else {
return []; return [];
} }

View File

@ -69,27 +69,35 @@ class SignupTable extends React.Component {
key: 'name', key: 'name',
render: (text, record, index) => { render: (text, record, index) => {
const items = [ const items = [
{id: 'Username', name: 'Username'}, {name: "Username", displayName: i18next.t("signup:Username")},
{id: 'ID', name: 'ID'}, {name: "ID", displayName: i18next.t("general:ID")},
{id: 'Display name', name: 'Display name'}, {name: "Display name", displayName: i18next.t("general:Display name")},
{id: 'Affiliation', name: 'Affiliation'}, {name: "Affiliation", displayName: i18next.t("user:Affiliation")},
{id: 'Country/Region', name: 'Country/Region'}, {name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{id: 'ID card', name: 'ID card'}, {name: "ID card", displayName: i18next.t("user:ID card")},
{id: 'Email', name: 'Email'}, {name: "Email", displayName: i18next.t("general:Email")},
{id: 'Password', name: 'Password'}, {name: "Password", displayName: i18next.t("forget:Password")},
{id: 'Confirm password', name: 'Confirm password'}, {name: "Confirm password", displayName: i18next.t("forget:Confirm")},
{id: 'Phone', name: 'Phone'}, {name: "Phone", displayName: i18next.t("general:Phone")},
{id: 'Agreement', name: 'Agreement'}, {name: "Agreement", displayName: i18next.t("signup:Agreement")},
]; ];
const getItemDisplayName = (text) => {
const item = items.filter(item => item.name === text);
if (item.length === 0) {
return "";
}
return item[0].displayName;
};
return ( return (
<Select virtual={false} style={{width: '100%'}} <Select virtual={false} style={{width: '100%'}}
value={text} value={getItemDisplayName(text)}
onChange={value => { onChange={value => {
this.updateField(table, index, 'name', value); this.updateField(table, index, 'name', value);
}} > }} >
{ {
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>) Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>)
} }
</Select> </Select>
) )
@ -156,7 +164,7 @@ class SignupTable extends React.Component {
} }
}, },
{ {
title: i18next.t("provider:rule"), title: i18next.t("application:rule"),
dataIndex: 'rule', dataIndex: 'rule',
key: 'rule', key: 'rule',
width: '155px', width: '155px',

View File

@ -119,8 +119,12 @@ class SyncerEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => { <Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => {
this.updateSyncerField('type', value); this.updateSyncerField('type', value);
this.state.syncer["tableColumns"] = Setting.getSyncerTableColumns(this.state.syncer); let syncer = this.state.syncer;
this.state.syncer.table = value === "Keycloak" ? "user_entity" : this.state.syncer.table; syncer["tableColumns"] = Setting.getSyncerTableColumns(this.state.syncer);
syncer.table = (value === "Keycloak") ? "user_entity" : this.state.syncer.table;
this.setState({
syncer: syncer,
});
})}> })}>
{ {
['Database', 'LDAP', 'Keycloak'] ['Database', 'LDAP', 'Keycloak']

View File

@ -22,7 +22,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class SyncerListPage extends BaseListPage { class SyncerListPage extends BaseListPage {
newSyncer() { newSyncer() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {

View File

@ -0,0 +1,59 @@
// Copyright 2021 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 sendTestEmail(provider, email) {
testEmailProvider(provider, email)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully send email`);
} else {
Setting.showMessage("error", res.msg);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
export function connectSmtpServer(provider) {
testEmailProvider(provider)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully connecting smtp server`);
} else {
Setting.showMessage("error", res.msg);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
function testEmailProvider(provider, email = "") {
let emailForm = {
title: provider.title,
content: provider.content,
sender: provider.displayName,
receivers: email === "" ? ["TestSmtpServer"] : [email],
provider: provider.name,
}
return fetch(`${Setting.ServerUrl}/api/send-email`, {
method: "POST",
credentials: "include",
body: JSON.stringify(emailForm)
}).then(res => res.json());
}

View File

@ -22,7 +22,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class TokenListPage extends BaseListPage { class TokenListPage extends BaseListPage {
newToken() { newToken() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {

View File

@ -124,46 +124,86 @@ class UserEditPage extends React.Component {
return (this.state.user.id === this.props.account?.id) || Setting.isAdminUser(this.props.account); return (this.state.user.id === this.props.account?.id) || Setting.isAdminUser(this.props.account);
} }
renderUser() { renderAccountItem(accountItem) {
return ( if (!accountItem.visible) {
<Card size="small" title={ return null;
<div> }
{this.state.mode === "add" ? i18next.t("user:New User") : i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button> const isSelf = this.state.user.id === this.props.account?.id;
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button> const isAdmin = Setting.isAdminUser(this.props.account);
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div> // return (
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> // <div>
// {
// JSON.stringify({accountItem: accountItem, isSelf: isSelf, isAdmin: isAdmin})
// }
// </div>
// )
if (accountItem.viewRule === "Self") {
if (!isSelf && !isAdmin) {
return null;
}
} else if (accountItem.viewRule === "Admin") {
if (!isAdmin) {
return null;
}
}
let disabled = false;
if (accountItem.modifyRule === "Self") {
if (!isSelf && !isAdmin) {
disabled = true;
}
} else if (accountItem.modifyRule === "Admin") {
if (!isAdmin) {
disabled = true;
}
} else if (accountItem.modifyRule === "Immutable") {
disabled = true;
}
if (accountItem.name === "Organization") {
return (
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: '10px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.user.owner} onChange={(value => {this.updateUserField('owner', value);})}> <Select virtual={false} style={{width: '100%'}} disabled={disabled} value={this.state.user.owner} onChange={(value => {this.updateUserField('owner', value);})}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "ID") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("ID", i18next.t("general:ID - Tooltip"))} : {Setting.getLabel("ID", i18next.t("general:ID - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.user.id} disabled={true} /> <Input value={this.state.user.id} disabled={disabled} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Name") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.user.name} disabled={!Setting.isAdminUser(this.props.account)} onChange={e => { <Input value={this.state.user.name} disabled={disabled} onChange={e => {
this.updateUserField('name', e.target.value); this.updateUserField('name', e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Display name") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
@ -174,6 +214,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Avatar") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} : {Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} :
@ -204,6 +247,9 @@ class UserEditPage extends React.Component {
</Row> </Row>
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "User type") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User type"), i18next.t("general:User type - Tooltip"))} : {Setting.getLabel(i18next.t("general:User type"), i18next.t("general:User type - Tooltip"))} :
@ -217,44 +263,56 @@ class UserEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Password") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} : {Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<PasswordModal user={this.state.user} account={this.props.account} disabled={this.state.userName !== this.state.user.name} /> <PasswordModal user={this.state.user} account={this.props.account} disabled={disabled} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Email") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} : {Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
</Col> </Col>
<Col style={{paddingRight: '20px'}} span={11} > <Col style={{paddingRight: '20px'}} span={11} >
<Input value={this.state.user.email} <Input value={this.state.user.email}
disabled={this.state.user.id === this.props.account?.id ? true : !Setting.isAdminUser(this.props.account)} disabled={disabled}
onChange={e => { onChange={e => {
this.updateUserField('email', e.target.value); this.updateUserField('email', e.target.value);
}} /> }} />
</Col> </Col>
<Col span={11} > <Col span={11} >
{ this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null} { this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null}
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Phone") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} : {Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
</Col> </Col>
<Col style={{paddingRight: '20px'}} span={11} > <Col style={{paddingRight: '20px'}} span={11} >
<Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`} <Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`}
disabled={this.state.user.id === this.props.account?.id ? true : !Setting.isAdminUser(this.props.account)} disabled={disabled}
onChange={e => { onChange={e => {
this.updateUserField('phone', e.target.value); this.updateUserField('phone', e.target.value);
}}/> }}/>
</Col> </Col>
<Col span={11} > <Col span={11} >
{ this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null} { this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Country/Region") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Country/Region"), i18next.t("user:Country/Region - Tooltip"))} : {Setting.getLabel(i18next.t("user:Country/Region"), i18next.t("user:Country/Region - Tooltip"))} :
@ -265,6 +323,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Location") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Location"), i18next.t("user:Location - Tooltip"))} : {Setting.getLabel(i18next.t("user:Location"), i18next.t("user:Location - Tooltip"))} :
@ -275,11 +336,15 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
{ )
(this.state.application === null || this.state.user === null) ? null : ( } else if (accountItem.name === "Affiliation") {
<AffiliationSelect labelSpan={(Setting.isMobile()) ? 22 : 2} application={this.state.application} user={this.state.user} onUpdateUserField={(key, value) => { return this.updateUserField(key, value)}} /> return (
) (this.state.application === null || this.state.user === null) ? null : (
} <AffiliationSelect labelSpan={(Setting.isMobile()) ? 22 : 2} application={this.state.application} user={this.state.user} onUpdateUserField={(key, value) => { return this.updateUserField(key, value)}} />
)
)
} else if (accountItem.name === "Title") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} : {Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
@ -290,6 +355,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Homepage") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Homepage"), i18next.t("user:Homepage - Tooltip"))} : {Setting.getLabel(i18next.t("user:Homepage"), i18next.t("user:Homepage - Tooltip"))} :
@ -300,6 +368,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Bio") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Bio"), i18next.t("user:Bio - Tooltip"))} : {Setting.getLabel(i18next.t("user:Bio"), i18next.t("user:Bio - Tooltip"))} :
@ -310,6 +381,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Tag") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} : {Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
@ -335,102 +409,138 @@ class UserEditPage extends React.Component {
} }
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Signup application") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Signup application"), i18next.t("general:Signup application - Tooltip"))} : {Setting.getLabel(i18next.t("general:Signup application"), i18next.t("general:Signup application - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.user.signupApplication} onChange={(value => {this.updateUserField('signupApplication', value);})}> <Select virtual={false} style={{width: '100%'}} disabled={disabled} value={this.state.user.signupApplication} onChange={(value => {this.updateUserField('signupApplication', value);})}>
{ {
this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>) this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
{ )
!this.isSelfOrAdmin() ? null : ( } else if (accountItem.name === "3rd-party logins") {
<Row style={{marginTop: '20px'}} > return (
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> !this.isSelfOrAdmin() ? null : (
{Setting.getLabel(i18next.t("user:3rd-party logins"), i18next.t("user:3rd-party logins - Tooltip"))} : <Row style={{marginTop: '20px'}} >
</Col> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
<Col span={22} > {Setting.getLabel(i18next.t("user:3rd-party logins"), i18next.t("user:3rd-party logins - Tooltip"))} :
<div style={{marginBottom: 20}}> </Col>
{ <Col span={22} >
(this.state.application === null || this.state.user === null) ? null : ( <div style={{marginBottom: 20}}>
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) => {
(providerItem.provider.category === "OAuth") ? ( (this.state.application === null || this.state.user === null) ? null : (
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} /> this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) =>
) : ( (providerItem.provider.category === "OAuth") ? (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} /> <OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} />
) ) : (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} />
) )
) )
} )
</div> }
</Col> </div>
</Row> </Col>
) </Row>
} )
)
} else if (accountItem.name === "Properties") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("user:Properties")}:
</Col>
<Col span={22} >
<CodeMirror
value={JSON.stringify(this.state.user.properties, null, 4)}
options={{mode: 'javascript', theme: "material-darker"}}
/>
</Col>
</Row>
)
} else if (accountItem.name === "Is admin") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{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 => {
this.updateUserField('isAdmin', checked);
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Is global admin") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{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 => {
this.updateUserField('isGlobalAdmin', checked);
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Is forbidden") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is forbidden"), i18next.t("user:Is forbidden - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isForbidden} onChange={checked => {
this.updateUserField('isForbidden', checked);
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Is deleted") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is deleted"), i18next.t("user:Is deleted - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isDeleted} onChange={checked => {
this.updateUserField('isDeleted', checked);
}} />
</Col>
</Row>
)
}
}
renderUser() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("user:New User") : i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
{ {
!Setting.isAdminUser(this.props.account) ? null : ( this.state.application?.organizationObj.accountItems?.map(accountItem => {
<React.Fragment> return (
{/*<Row style={{marginTop: '20px'}} >*/} <React.Fragment key={accountItem.name}>
{/* <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>*/} {
{/* {i18next.t("user:Properties")}:*/} this.renderAccountItem(accountItem)
{/* </Col>*/} }
{/* <Col span={22} >*/} </React.Fragment>
{/* <CodeMirror*/} )
{/* value={JSON.stringify(this.state.user.properties, null, 4)}*/} })
{/* options={{mode: 'javascript', theme: "material-darker"}}*/}
{/* />*/}
{/* </Col>*/}
{/*</Row>*/}
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{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 => {
this.updateUserField('isAdmin', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{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 => {
this.updateUserField('isGlobalAdmin', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is forbidden"), i18next.t("user:Is forbidden - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isForbidden} onChange={checked => {
this.updateUserField('isForbidden', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is deleted"), i18next.t("user:Is deleted - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isDeleted} onChange={checked => {
this.updateUserField('isDeleted', checked);
}} />
</Col>
</Row>
</React.Fragment>
)
} }
</Card> </Card>
) )
} }
@ -477,7 +587,7 @@ class UserEditPage extends React.Component {
return ( return (
<div> <div>
{ {
this.state.loading ? <Spin loading={this.state.loading} size="large" /> : ( this.state.loading ? <Spin size="large" /> : (
this.state.user !== null ? this.renderUser() : this.state.user !== null ? this.renderUser() :
<Result <Result
status="404" status="404"

View File

@ -0,0 +1,32 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";
function Icon({width = 24, height = 24, color}) {
return <img src={`${StaticBaseUrl}/buttons/douyin.svg`} alt="Sign in with Douyin" style={{width: 24, height: 24}}/>;
}
const config = {
text: "Sign in with Douyin",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const DouyinLoginButton = createButton(config);
export default DouyinLoginButton;

View File

@ -44,10 +44,13 @@ import AzureADLoginButton from "./AzureADLoginButton";
import SlackLoginButton from "./SlackLoginButton"; import SlackLoginButton from "./SlackLoginButton";
import SteamLoginButton from "./SteamLoginButton"; import SteamLoginButton from "./SteamLoginButton";
import OktaLoginButton from "./OktaLoginButton"; import OktaLoginButton from "./OktaLoginButton";
import DouyinLoginButton from "./DouyinLoginButton";
import CustomGithubCorner from "../CustomGithubCorner"; import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput"; import {CountDownInput} from "../common/CountDownInput";
import BilibiliLoginButton from "./BilibiliLoginButton"; import BilibiliLoginButton from "./BilibiliLoginButton";
/* eslint-disable jsx-a11y/anchor-is-valid */
class LoginPage extends React.Component { class LoginPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -143,47 +146,48 @@ class LoginPage extends React.Component {
const application = this.getApplicationObj(); const application = this.getApplicationObj();
const ths = this; const ths = this;
//here we are supposed to judge whether casdoor is working as a oauth server or CAS server // here we are supposed to determine whether Casdoor is working as an OAuth server or CAS server
if (this.state.type === "cas") { if (this.state.type === "cas") {
//cas // CAS
const casParams = Util.getCasParameters() const casParams = Util.getCasParameters();
values["type"] = this.state.type; values["type"] = this.state.type;
AuthBackend.loginCas(values, casParams).then((res) => { AuthBackend.loginCas(values, casParams).then((res) => {
if (res.status === 'ok') { if (res.status === 'ok') {
let msg = "Logged in successfully. " let msg = "Logged in successfully. ";
if (casParams.service === "") { if (casParams.service === "") {
//If service was not specified, CAS MUST display a message notifying the client that it has successfully initiated a single sign-on session. // 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." msg += "Now you can visit apps protected by Casdoor.";
} }
Util.showMessage("success", msg); Util.showMessage("success", msg);
if (casParams.service !== "") {
let st = res.data
window.location.href = casParams.service + "?ticket=" + st
}
if (casParams.service !== "") {
let st = res.data;
let newUrl = new URL(casParams.service);
newUrl.searchParams.append("ticket", st);
window.location.href = newUrl.toString();
}
} else { } else {
Util.showMessage("error", `Failed to log in: ${res.msg}`); Util.showMessage("error", `Failed to log in: ${res.msg}`);
} }
}) })
} else { } else {
//oauth // OAuth
const oAuthParams = Util.getOAuthGetParameters(); const oAuthParams = Util.getOAuthGetParameters();
if (oAuthParams !== null && oAuthParams.responseType != null && oAuthParams.responseType !== "") { if (oAuthParams !== null && oAuthParams.responseType != null && oAuthParams.responseType !== "") {
values["type"] = oAuthParams.responseType values["type"] = oAuthParams.responseType;
}else{ } else {
values["type"] = this.state.type; values["type"] = this.state.type;
} }
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix; values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
if (oAuthParams !== null){ if (oAuthParams !== null) {
values["samlRequest"] = oAuthParams.samlRequest; values["samlRequest"] = oAuthParams.samlRequest;
} }
if (values["samlRequest"] != null && values["samlRequest"] !== "") { if (values["samlRequest"] != null && values["samlRequest"] !== "") {
values["type"] = "saml"; values["type"] = "saml";
} }
AuthBackend.login(values, oAuthParams) AuthBackend.login(values, oAuthParams)
.then((res) => { .then((res) => {
if (res.status === 'ok') { if (res.status === 'ok') {
@ -284,6 +288,8 @@ class LoginPage extends React.Component {
return <BilibiliLoginButton text={text} align={"center"} /> return <BilibiliLoginButton text={text} align={"center"} />
} else if (type === "Okta") { } else if (type === "Okta") {
return <OktaLoginButton text={text} align={"center"} /> return <OktaLoginButton text={text} align={"center"} />
} else if (type === "Douyin") {
return <DouyinLoginButton text={text} align={"center"} />
} }
return text; return text;

View File

@ -111,6 +111,10 @@ const authInfo = {
scope: "openid%20profile%20email", scope: "openid%20profile%20email",
endpoint: "http://example.com", endpoint: "http://example.com",
}, },
Douyin: {
scope: "user_info",
endpoint: "https://open.douyin.com/platform/oauth/connect",
},
Custom: { Custom: {
endpoint: "https://example.com/", endpoint: "https://example.com/",
}, },
@ -239,6 +243,8 @@ export function getAuthUrl(application, provider, method) {
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`; return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`;
} else if (provider.type === "Okta") { } else if (provider.type === "Okta") {
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Douyin") {
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Custom") { } else if (provider.type === "Custom") {
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`; return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`;
} else if (provider.type === "Bilibili") { } else if (provider.type === "Bilibili") {

View File

@ -25,6 +25,8 @@ import {CountDownInput} from "../common/CountDownInput";
import SelectRegionBox from "../SelectRegionBox"; import SelectRegionBox from "../SelectRegionBox";
import CustomGithubCorner from "../CustomGithubCorner"; import CustomGithubCorner from "../CustomGithubCorner";
/* eslint-disable jsx-a11y/anchor-is-valid */
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
xs: { xs: {

View File

@ -0,0 +1,56 @@
// Copyright 2021 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 getModels(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-models?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getModel(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-model?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateModel(owner, name, model) {
let newModel = Setting.deepCopy(model);
return fetch(`${Setting.ServerUrl}/api/update-model?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newModel),
}).then(res => res.json());
}
export function addModel(model) {
let newModel = Setting.deepCopy(model);
return fetch(`${Setting.ServerUrl}/api/add-model`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newModel),
}).then(res => res.json());
}
export function deleteModel(model) {
let newModel = Setting.deepCopy(model);
return fetch(`${Setting.ServerUrl}/api/delete-model`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newModel),
}).then(res => res.json());
}

View File

@ -112,6 +112,30 @@ export function sendCode(checkType, checkId, checkKey, dest, type, orgId, checkU
}); });
} }
export function verifyCaptcha(captchaType, captchaToken, clientSecret) {
let formData = new FormData();
formData.append("captchaType", captchaType);
formData.append("captchaToken", captchaToken);
formData.append("clientSecret", clientSecret);
return fetch(`${Setting.ServerUrl}/api/verify-captcha`, {
method: "POST",
credentials: "include",
body: formData
}).then(res => res.json()).then(res => {
if (res.status === "ok") {
if (res.data) {
Setting.showMessage("success", i18next.t("user:Captcha Verify Success"));
} else {
Setting.showMessage("error", i18next.t("user:Captcha Verify Failed"));
}
return true;
} else {
Setting.showMessage("error", i18next.t("user:" + res.msg));
return false;
}
});
}
export function resetEmailOrPhone(dest, type, code) { export function resetEmailOrPhone(dest, type, code) {
let formData = new FormData(); let formData = new FormData();
formData.append("dest", dest); formData.append("dest", dest);
@ -124,8 +148,8 @@ export function resetEmailOrPhone(dest, type, code) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function getHumanCheck() { export function getCaptcha(owner, name, isCurrentProvider) {
return fetch(`${Setting.ServerUrl}/api/get-human-check`, { return fetch(`${Setting.ServerUrl}/api/get-captcha?applicationId=${owner}/${encodeURIComponent(name)}&isCurrentProvider=${isCurrentProvider}`, {
method: "GET" method: "GET"
}).then(res => res.json()); }).then(res => res.json()).then(res => res.data);
} }

View File

@ -92,7 +92,7 @@ class HomePage extends React.Component {
) )
} else { } else {
return ( return (
<div style={{marginRight:'15px',marginLeft:'15px'}}> <div style={{marginRight: "15px", marginLeft: "15px"}}>
<Row style={{marginLeft: "-20px", marginRight: "-20px", marginTop: "20px"}} gutter={24}> <Row style={{marginLeft: "-20px", marginRight: "-20px", marginTop: "20px"}} gutter={24}>
{ {
items.map(item => { items.map(item => {

View File

@ -0,0 +1,139 @@
// 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 { Button, Col, Input, Modal, Row } from "antd";
import React from "react";
import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend";
import * as ProviderBackend from "../backend/ProviderBackend";
import { SafetyOutlined } from "@ant-design/icons";
import { CaptchaWidget } from "./CaptchaWidget";
export const CaptchaPreview = ({ provider, providerName, clientSecret, captchaType, owner, clientId, name, providerUrl }) => {
const [visible, setVisible] = React.useState(false);
const [captchaImg, setCaptchaImg] = React.useState("");
const [captchaToken, setCaptchaToken] = React.useState("");
const [secret, setSecret] = React.useState(clientSecret);
const handleOk = () => {
UserBackend.verifyCaptcha(
captchaType,
captchaToken,
secret
).then(() => {
setCaptchaToken("");
setVisible(false);
});
};
const handleCancel = () => {
setVisible(false);
};
const getCaptchaFromBackend = () => {
UserBackend.getCaptcha(owner, name, true).then((res) => {
if (captchaType === "Default") {
setSecret(res.captchaId);
setCaptchaImg(res.captchaImage);
} else {
setSecret(res.clientSecret);
}
});
}
const clickPreview = () => {
setVisible(true);
provider.name = name;
provider.clientId = clientId;
provider.type = captchaType;
provider.providerUrl = providerUrl;
if (clientSecret !== "***") {
provider.clientSecret = clientSecret;
ProviderBackend.updateProvider(owner, providerName, provider).then(() => {
getCaptchaFromBackend();
});
} else {
getCaptchaFromBackend();
}
};
const renderDefaultCaptcha = () => {
return (
<Col>
<Row
style={{
backgroundImage: `url('data:image/png;base64,${captchaImg}')`,
backgroundRepeat: "no-repeat",
height: "80px",
width: "200px",
borderRadius: "3px",
border: "1px solid #ccc",
marginBottom: 10,
}}
/>
<Row>
<Input
autoFocus
value={captchaToken}
prefix={<SafetyOutlined />}
placeholder={i18next.t("general:Captcha")}
onPressEnter={handleOk}
onChange={(e) => setCaptchaToken(e.target.value)}
/>
</Row>
</Col>
);
};
const onSubmit = (token) => {
setCaptchaToken(token);
};
const renderCheck = () => {
if (captchaType === "Default") {
return renderDefaultCaptcha();
} else {
return (
<CaptchaWidget
captchaType={captchaType}
siteKey={clientId}
onChange={onSubmit}
/>
);
}
};
return (
<React.Fragment>
<Button style={{ fontSize: 14 }} type={"primary"} onClick={clickPreview}>
{i18next.t("general:Preview")}
</Button>
<Modal
closable={false}
maskClosable={false}
destroyOnClose={true}
title={i18next.t("general:Captcha")}
visible={visible}
okText={i18next.t("user:OK")}
cancelText={i18next.t("user:Cancel")}
onOk={handleOk}
onCancel={handleCancel}
width={348}
>
{renderCheck()}
</Modal>
</React.Fragment>
);
};

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 React, { useEffect } from "react";
export const CaptchaWidget = ({ captchaType, siteKey, onChange }) => {
const loadScript = (src) => {
var tag = document.createElement("script");
tag.async = false;
tag.src = src;
var body = document.getElementsByTagName("body")[0];
body.appendChild(tag);
};
useEffect(() => {
switch (captchaType) {
case "reCAPTCHA":
const reTimer = setInterval(() => {
if (!window.grecaptcha) {
loadScript("https://recaptcha.net/recaptcha/api.js");
}
if (window.grecaptcha && window.grecaptcha.render) {
window.grecaptcha.render("captcha", {
sitekey: siteKey,
callback: onChange,
});
clearInterval(reTimer);
}
}, 300);
break;
case "hCaptcha":
const hTimer = setInterval(() => {
if (!window.hcaptcha) {
loadScript("https://js.hcaptcha.com/1/api.js");
}
if (window.hcaptcha && window.hcaptcha.render) {
window.hcaptcha.render("captcha", {
sitekey: siteKey,
callback: onChange,
});
clearInterval(hTimer);
}
}, 300);
break;
default:
break;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [captchaType, siteKey]);
return <div id="captcha"></div>;
};

View File

@ -18,6 +18,8 @@ import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import {SafetyOutlined} from "@ant-design/icons"; import {SafetyOutlined} from "@ant-design/icons";
import {authConfig} from "../auth/Auth";
import { CaptchaWidget } from "./CaptchaWidget";
const { Search } = Input; const { Search } = Input;
@ -30,6 +32,8 @@ export const CountDownInput = (props) => {
const [checkId, setCheckId] = React.useState(""); const [checkId, setCheckId] = React.useState("");
const [buttonLeftTime, setButtonLeftTime] = React.useState(0); const [buttonLeftTime, setButtonLeftTime] = React.useState(0);
const [buttonLoading, setButtonLoading] = React.useState(false); const [buttonLoading, setButtonLoading] = React.useState(false);
const [buttonDisabled, setButtonDisabled] = React.useState(true);
const [clientId, setClientId] = React.useState("");
const handleCountDown = (leftTime = 60) => { const handleCountDown = (leftTime = 60) => {
let leftTimeSecond = leftTime let leftTimeSecond = leftTime
@ -62,14 +66,23 @@ export const CountDownInput = (props) => {
setKey(""); setKey("");
} }
const loadHumanCheck = () => { const loadCaptcha = () => {
UserBackend.getHumanCheck().then(res => { UserBackend.getCaptcha("admin", authConfig.appName, false).then(res => {
if (res.type === "none") { if (res.type === "none") {
UserBackend.sendCode("none", "", "", ...onButtonClickArgs); UserBackend.sendCode("none", "", "", ...onButtonClickArgs).then(res => {
} else if (res.type === "captcha") { if (res) {
handleCountDown(60);
}
});
} else if (res.type === "Default") {
setCheckId(res.captchaId); setCheckId(res.captchaId);
setCaptchaImg(res.captchaImage); setCaptchaImg(res.captchaImage);
setCheckType("captcha"); setCheckType("Default");
setVisible(true);
} else if (res.type === "reCAPTCHA" || res.type === "hCaptcha") {
setCheckType(res.type);
setClientId(res.clientId);
setCheckId(res.clientSecret);
setVisible(true); setVisible(true);
} else { } else {
Setting.showMessage("error", i18next.t("signup:Unknown Check Type")); Setting.showMessage("error", i18next.t("signup:Unknown Check Type"));
@ -98,9 +111,23 @@ export const CountDownInput = (props) => {
) )
} }
const onSubmit = (token) => {
setButtonDisabled(false);
setKey(token);
}
const renderCheck = () => { const renderCheck = () => {
if (checkType === "captcha") return renderCaptcha(); if (checkType === "Default") {
return null; return renderCaptcha();
} else {
return (
<CaptchaWidget
captchaType={checkType}
siteKey={clientId}
onChange={onSubmit}
/>
);
}
} }
return ( return (
@ -116,7 +143,7 @@ export const CountDownInput = (props) => {
{buttonLeftTime > 0 ? `${buttonLeftTime} s` : buttonLoading ? i18next.t("code:Sending Code") : i18next.t("code:Send Code")} {buttonLeftTime > 0 ? `${buttonLeftTime} s` : buttonLoading ? i18next.t("code:Sending Code") : i18next.t("code:Send Code")}
</Button> </Button>
} }
onSearch={loadHumanCheck} onSearch={loadCaptcha}
/> />
<Modal <Modal
closable={false} closable={false}
@ -128,8 +155,8 @@ export const CountDownInput = (props) => {
cancelText={i18next.t("user:Cancel")} cancelText={i18next.t("user:Cancel")}
onOk={handleOk} onOk={handleOk}
onCancel={handleCancel} onCancel={handleCancel}
okButtonProps={{disabled: key.length !== 5}} okButtonProps={{disabled: key.length !== 5 && buttonDisabled}}
width={248} width={348}
> >
{ {
renderCheck() renderCheck()

View File

@ -142,7 +142,7 @@ class OAuthWidget extends React.Component {
</span> </span>
</Col> </Col>
<Col span={24 - this.props.labelSpan} > <Col span={24 - this.props.labelSpan} >
<img style={{marginRight: '10px'}} width={30} height={30} src={avatarUrl} alt={name} /> <img style={{marginRight: '10px'}} width={30} height={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
<span style={{width: this.props.labelSpan === 3 ? '300px' : '130px', display: (Setting.isMobile()) ? 'inline' : "inline-block"}}> <span style={{width: this.props.labelSpan === 3 ? '300px' : '130px', display: (Setting.isMobile()) ? 'inline' : "inline-block"}}>
{ {
linkedValue === "" ? ( linkedValue === "" ? (

View File

@ -6,6 +6,9 @@
"Sign Up": "Registrieren" "Sign Up": "Registrieren"
}, },
"application": { "application": {
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
"Copy signup page URL": "Copy signup page URL",
"Edit Application": "Anwendung bearbeiten", "Edit Application": "Anwendung bearbeiten",
"Enable code signin": "Code-Anmeldung aktivieren", "Enable code signin": "Code-Anmeldung aktivieren",
"Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip", "Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip",
@ -19,21 +22,24 @@
"Password ON": "Passwort AN", "Password ON": "Passwort AN",
"Password ON - Tooltip": "Whether to allow password login", "Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei", "Please select a HTML file": "Bitte wählen Sie eine HTML-Datei",
"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": "Weiterleitungs-URL", "Redirect URL": "Weiterleitungs-URL",
"Redirect URLs": "Umleitungs-URLs", "Redirect URLs": "Umleitungs-URLs",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Aktualisierungs-Token läuft ab", "Refresh token expire": "Aktualisierungs-Token läuft ab",
"Refresh token expire - Tooltip": "Aktualisierungs-Token läuft ab - Tooltip", "Refresh token expire - Tooltip": "Aktualisierungs-Token läuft ab - Tooltip",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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": "Anmeldesitzung", "Signin session": "Anmeldesitzung",
"Signup items": "Artikel registrieren", "Signup items": "Artikel registrieren",
"Signup items - Tooltip": "Signup items that need to be filled in when users register", "Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "Test-Nachfrageseite..", "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",
"Test signin page..": "Anmeldeseite testen..",
"Test signup page..": "Anmeldeseite testen..",
"Token expire": "Token läuft ab", "Token expire": "Token läuft ab",
"Token expire - Tooltip": "Token läuft ab - Tooltip", "Token expire - Tooltip": "Token läuft ab - Tooltip",
"Token format": "Token-Format", "Token format": "Token-Format",
"Token format - Tooltip": "Token-Format - Tooltip" "Token format - Tooltip": "Token-Format - Tooltip",
"rule": "rule"
}, },
"cert": { "cert": {
"Bit size": "Bitgröße", "Bit size": "Bitgröße",
@ -99,6 +105,7 @@
"Cert": "Cert", "Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip", "Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs", "Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "Client-IP", "Client IP": "Client-IP",
"Created time": "Erstellte Zeit", "Created time": "Erstellte Zeit",
"Default avatar": "Standard Avatar", "Default avatar": "Standard Avatar",
@ -131,6 +138,9 @@
"Master password": "Master-Passwort", "Master password": "Master-Passwort",
"Master password - Tooltip": "Masterpasswort - Tooltip", "Master password - Tooltip": "Masterpasswort - Tooltip",
"Method": "Methode", "Method": "Methode",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Name", "Name": "Name",
"Name - Tooltip": "Unique string-style identifier", "Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "OAuth-Anbieter", "OAuth providers": "OAuth-Anbieter",
@ -243,7 +253,15 @@
"sign up now": "jetzt anmelden", "sign up now": "jetzt anmelden",
"username, Email or phone": "Benutzername, E-Mail oder Telefon" "username, Email or phone": "Benutzername, E-Mail oder Telefon"
}, },
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": { "organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Standard Avatar", "Default avatar": "Standard Avatar",
"Edit Organization": "Organisation bearbeiten", "Edit Organization": "Organisation bearbeiten",
"Favicon": "Févicon", "Favicon": "Févicon",
@ -255,7 +273,9 @@
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip", "Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website-URL", "Website URL": "Website-URL",
"Website URL - Tooltip": "Unique string-style identifier" "Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
}, },
"payment": { "payment": {
"Confirm your invoice information": "Confirm your invoice information", "Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +407,7 @@
"Domain": "Domäne", "Domain": "Domäne",
"Domain - Tooltip": "Storage endpoint custom domain", "Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "Anbieter bearbeiten", "Edit Provider": "Anbieter bearbeiten",
"Email Content": "Email Content", "Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier", "Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "E-Mail-Titel", "Email Title": "E-Mail-Titel",
"Email Title - Tooltip": "Unique string-style identifier", "Email Title - Tooltip": "Unique string-style identifier",
@ -425,7 +445,10 @@
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Geheimer Zugangsschlüssel", "Secret access key": "Geheimer Zugangsschlüssel",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip", "SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
"Send Test Email": "Send Test Email",
"Sign Name": "Schild Name", "Sign Name": "Schild Name",
"Sign Name - Tooltip": "Unique string-style identifier", "Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Signaturanfrage", "Sign request": "Signaturanfrage",
@ -436,12 +459,17 @@
"Signup HTML": "HTML registrieren", "Signup HTML": "HTML registrieren",
"Signup HTML - Edit": "HTML registrieren - Bearbeiten", "Signup HTML - Edit": "HTML registrieren - Bearbeiten",
"Signup HTML - Tooltip": "HTML registrieren - Tooltip", "Signup HTML - Tooltip": "HTML registrieren - Tooltip",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type", "Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip", "Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Vorlagencode", "Template Code": "Vorlagencode",
"Template Code - Tooltip": "Unique string-style identifier", "Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Nutzungsbedingungen", "Terms of Use": "Nutzungsbedingungen",
"Terms of Use - Tooltip": "Nutzungsbedingungen - Tooltip", "Terms of Use - Tooltip": "Nutzungsbedingungen - Tooltip",
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"Token URL": "Token URL", "Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip", "Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Typ", "Type": "Typ",
@ -454,7 +482,6 @@
"canUnlink": "canUnlink", "canUnlink": "canUnlink",
"prompted": "gefragt", "prompted": "gefragt",
"required": "benötigt", "required": "benötigt",
"rule": "regel",
"visible": "sichtbar" "visible": "sichtbar"
}, },
"record": { "record": {
@ -483,6 +510,7 @@
}, },
"signup": { "signup": {
"Accept": "Akzeptieren", "Accept": "Akzeptieren",
"Agreement": "Agreement",
"Confirm": "Bestätigen", "Confirm": "Bestätigen",
"Decline": "Ablehnen", "Decline": "Ablehnen",
"Have account?": "Haben Sie Konto?", "Have account?": "Haben Sie Konto?",
@ -558,6 +586,8 @@
"Bio": "Bio", "Bio": "Bio",
"Bio - Tooltip": "Bio - Tooltip", "Bio - Tooltip": "Bio - Tooltip",
"Cancel": "Abbrechen", "Cancel": "Abbrechen",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code gesendet", "Code Sent": "Code gesendet",
"Country/Region": "Land/Region", "Country/Region": "Land/Region",
"Country/Region - Tooltip": "Country/Region", "Country/Region - Tooltip": "Country/Region",

View File

@ -6,6 +6,9 @@
"Sign Up": "Sign Up" "Sign Up": "Sign Up"
}, },
"application": { "application": {
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
"Copy signup page URL": "Copy signup page URL",
"Edit Application": "Edit Application", "Edit Application": "Edit Application",
"Enable code signin": "Enable code signin", "Enable code signin": "Enable code signin",
"Enable code signin - Tooltip": "Enable code signin - Tooltip", "Enable code signin - Tooltip": "Enable code signin - Tooltip",
@ -19,21 +22,24 @@
"Password ON": "Password ON", "Password ON": "Password ON",
"Password ON - Tooltip": "Password ON - Tooltip", "Password ON - Tooltip": "Password ON - Tooltip",
"Please select a HTML file": "Please select a HTML file", "Please select a HTML file": "Please select a HTML file",
"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": "Redirect URL", "Redirect URL": "Redirect URL",
"Redirect URLs": "Redirect URLs", "Redirect URLs": "Redirect URLs",
"Redirect URLs - Tooltip": "Redirect URLs - Tooltip", "Redirect URLs - Tooltip": "Redirect URLs - Tooltip",
"Refresh token expire": "Refresh token expire", "Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip", "Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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", "Signin session": "Signin session",
"Signup items": "Signup items", "Signup items": "Signup items",
"Signup items - Tooltip": "Signup items - Tooltip", "Signup items - Tooltip": "Signup items - Tooltip",
"Test prompt page..": "Test prompt page..", "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",
"Test signin page..": "Test signin page..",
"Test signup page..": "Test signup page..",
"Token expire": "Token expire", "Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip", "Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip",
"rule": "rule"
}, },
"cert": { "cert": {
"Bit size": "Bit size", "Bit size": "Bit size",
@ -99,6 +105,7 @@
"Cert": "Cert", "Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip", "Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs", "Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -131,6 +138,9 @@
"Master password": "Master password", "Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip", "Master password - Tooltip": "Master password - Tooltip",
"Method": "Method", "Method": "Method",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Name", "Name": "Name",
"Name - Tooltip": "Name - Tooltip", "Name - Tooltip": "Name - Tooltip",
"OAuth providers": "OAuth providers", "OAuth providers": "OAuth providers",
@ -243,7 +253,15 @@
"sign up now": "sign up now", "sign up now": "sign up now",
"username, Email or phone": "username, Email or phone" "username, Email or phone": "username, Email or phone"
}, },
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": { "organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
"Edit Organization": "Edit Organization", "Edit Organization": "Edit Organization",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -255,7 +273,9 @@
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip", "Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL", "Website URL": "Website URL",
"Website URL - Tooltip": "Website URL - Tooltip" "Website URL - Tooltip": "Website URL - Tooltip",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
}, },
"payment": { "payment": {
"Confirm your invoice information": "Confirm your invoice information", "Confirm your invoice information": "Confirm your invoice information",
@ -387,9 +407,9 @@
"Domain": "Domain", "Domain": "Domain",
"Domain - Tooltip": "Domain - Tooltip", "Domain - Tooltip": "Domain - Tooltip",
"Edit Provider": "Edit Provider", "Edit Provider": "Edit Provider",
"Email Content": "Email Content", "Email Content": "Email content",
"Email Content - Tooltip": "Email Content - Tooltip", "Email Content - Tooltip": "Email Content - Tooltip",
"Email Title": "Email Title", "Email Title": "Email title",
"Email Title - Tooltip": "Email Title - Tooltip", "Email Title - Tooltip": "Email Title - Tooltip",
"Endpoint": "Endpoint", "Endpoint": "Endpoint",
"Endpoint (Intranet)": "Endpoint (Intranet)", "Endpoint (Intranet)": "Endpoint (Intranet)",
@ -425,7 +445,10 @@
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Secret access key", "Secret access key": "Secret access key",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip", "SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
"Send Test Email": "Send Test Email",
"Sign Name": "Sign Name", "Sign Name": "Sign Name",
"Sign Name - Tooltip": "Sign Name - Tooltip", "Sign Name - Tooltip": "Sign Name - Tooltip",
"Sign request": "Sign request", "Sign request": "Sign request",
@ -436,12 +459,17 @@
"Signup HTML": "Signup HTML", "Signup HTML": "Signup HTML",
"Signup HTML - Edit": "Signup HTML - Edit", "Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Signup HTML - Tooltip", "Signup HTML - Tooltip": "Signup HTML - Tooltip",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type", "Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip", "Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Template Code", "Template Code": "Template Code",
"Template Code - Tooltip": "Template Code - Tooltip", "Template Code - Tooltip": "Template Code - Tooltip",
"Terms of Use": "Terms of Use", "Terms of Use": "Terms of Use",
"Terms of Use - Tooltip": "Terms of Use - Tooltip", "Terms of Use - Tooltip": "Terms of Use - Tooltip",
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"Token URL": "Token URL", "Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip", "Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Type", "Type": "Type",
@ -454,7 +482,6 @@
"canUnlink": "canUnlink", "canUnlink": "canUnlink",
"prompted": "prompted", "prompted": "prompted",
"required": "required", "required": "required",
"rule": "rule",
"visible": "visible" "visible": "visible"
}, },
"record": { "record": {
@ -483,6 +510,7 @@
}, },
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Agreement": "Agreement",
"Confirm": "Confirm", "Confirm": "Confirm",
"Decline": "Decline", "Decline": "Decline",
"Have account?": "Have account?", "Have account?": "Have account?",
@ -558,6 +586,8 @@
"Bio": "Bio", "Bio": "Bio",
"Bio - Tooltip": "Bio - Tooltip", "Bio - Tooltip": "Bio - Tooltip",
"Cancel": "Cancel", "Cancel": "Cancel",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code Sent", "Code Sent": "Code Sent",
"Country/Region": "Country/Region", "Country/Region": "Country/Region",
"Country/Region - Tooltip": "Country/Region - Tooltip", "Country/Region - Tooltip": "Country/Region - Tooltip",

View File

@ -6,6 +6,9 @@
"Sign Up": "S'inscrire" "Sign Up": "S'inscrire"
}, },
"application": { "application": {
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
"Copy signup page URL": "Copy signup page URL",
"Edit Application": "Modifier l'application", "Edit Application": "Modifier l'application",
"Enable code signin": "Activer la connexion au code", "Enable code signin": "Activer la connexion au code",
"Enable code signin - Tooltip": "Activer la connexion au code - infobulle", "Enable code signin - Tooltip": "Activer la connexion au code - infobulle",
@ -19,21 +22,24 @@
"Password ON": "Mot de passe activé", "Password ON": "Mot de passe activé",
"Password ON - Tooltip": "Whether to allow password login", "Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML", "Please select a HTML file": "Veuillez sélectionner un fichier 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 redirection", "Redirect URL": "URL de redirection",
"Redirect URLs": "URL de redirection", "Redirect URLs": "URL de redirection",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Expiration du jeton d'actualisation", "Refresh token expire": "Expiration du jeton d'actualisation",
"Refresh token expire - Tooltip": "Expiration du jeton d'actualisation - infobulle", "Refresh token expire - Tooltip": "Expiration du jeton d'actualisation - infobulle",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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": "Connexion à la session", "Signin session": "Connexion à la session",
"Signup items": "Inscrire des éléments", "Signup items": "Inscrire des éléments",
"Signup items - Tooltip": "Signup items that need to be filled in when users register", "Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "Tester la vitesse d'exécution.", "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",
"Test signin page..": "Tester la connexion en ligne.",
"Test signup page..": "Tester l'inscription.",
"Token expire": "Expiration du jeton", "Token expire": "Expiration du jeton",
"Token expire - Tooltip": "Expiration du jeton - Info-bulle", "Token expire - Tooltip": "Expiration du jeton - Info-bulle",
"Token format": "Format du jeton", "Token format": "Format du jeton",
"Token format - Tooltip": "Format du jeton - infobulle" "Token format - Tooltip": "Format du jeton - infobulle",
"rule": "rule"
}, },
"cert": { "cert": {
"Bit size": "Taille du bit", "Bit size": "Taille du bit",
@ -99,6 +105,7 @@
"Cert": "Cert", "Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip", "Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certes", "Certs": "Certes",
"Click to Upload": "Click to Upload",
"Client IP": "IP du client", "Client IP": "IP du client",
"Created time": "Date de création", "Created time": "Date de création",
"Default avatar": "Avatar par défaut", "Default avatar": "Avatar par défaut",
@ -131,6 +138,9 @@
"Master password": "Mot de passe maître", "Master password": "Mot de passe maître",
"Master password - Tooltip": "Mot de passe maître - Infobulle", "Master password - Tooltip": "Mot de passe maître - Infobulle",
"Method": "Méthode", "Method": "Méthode",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Nom", "Name": "Nom",
"Name - Tooltip": "Unique string-style identifier", "Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "Fournisseurs OAuth", "OAuth providers": "Fournisseurs OAuth",
@ -243,7 +253,15 @@
"sign up now": "inscrivez-vous maintenant", "sign up now": "inscrivez-vous maintenant",
"username, Email or phone": "nom d'utilisateur, e-mail ou téléphone" "username, Email or phone": "nom d'utilisateur, e-mail ou téléphone"
}, },
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": { "organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Avatar par défaut", "Default avatar": "Avatar par défaut",
"Edit Organization": "Modifier l'organisation", "Edit Organization": "Modifier l'organisation",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -255,7 +273,9 @@
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip", "Tags - Tooltip": "Tags - Tooltip",
"Website URL": "URL du site web", "Website URL": "URL du site web",
"Website URL - Tooltip": "Unique string-style identifier" "Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
}, },
"payment": { "payment": {
"Confirm your invoice information": "Confirm your invoice information", "Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +407,7 @@
"Domain": "Domaine", "Domain": "Domaine",
"Domain - Tooltip": "Storage endpoint custom domain", "Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "Modifier le fournisseur", "Edit Provider": "Modifier le fournisseur",
"Email Content": "Email Content", "Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier", "Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "Titre de l'e-mail", "Email Title": "Titre de l'e-mail",
"Email Title - Tooltip": "Unique string-style identifier", "Email Title - Tooltip": "Unique string-style identifier",
@ -425,7 +445,10 @@
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Clé d'accès secrète", "Secret access key": "Clé d'accès secrète",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Infobulle", "SecretAccessKey - Tooltip": "SecretAccessKey - Infobulle",
"Send Test Email": "Send Test Email",
"Sign Name": "Nom du panneau", "Sign Name": "Nom du panneau",
"Sign Name - Tooltip": "Unique string-style identifier", "Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Demande de signature", "Sign request": "Demande de signature",
@ -436,12 +459,17 @@
"Signup HTML": "Inscription HTML", "Signup HTML": "Inscription HTML",
"Signup HTML - Edit": "Inscription HTML - Modifier", "Signup HTML - Edit": "Inscription HTML - Modifier",
"Signup HTML - Tooltip": "Inscription HTML - infobulle", "Signup HTML - Tooltip": "Inscription HTML - infobulle",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type", "Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip", "Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Code du modèle", "Template Code": "Code du modèle",
"Template Code - Tooltip": "Unique string-style identifier", "Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Conditions d'utilisation", "Terms of Use": "Conditions d'utilisation",
"Terms of Use - Tooltip": "Conditions d'utilisation - Info-bulle", "Terms of Use - Tooltip": "Conditions d'utilisation - Info-bulle",
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"Token URL": "Token URL", "Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip", "Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Type de texte", "Type": "Type de texte",
@ -454,7 +482,6 @@
"canUnlink": "canUnlink", "canUnlink": "canUnlink",
"prompted": "invitée", "prompted": "invitée",
"required": "Obligatoire", "required": "Obligatoire",
"rule": "règle",
"visible": "Visible" "visible": "Visible"
}, },
"record": { "record": {
@ -483,6 +510,7 @@
}, },
"signup": { "signup": {
"Accept": "Accepter", "Accept": "Accepter",
"Agreement": "Agreement",
"Confirm": "Valider", "Confirm": "Valider",
"Decline": "Refuser", "Decline": "Refuser",
"Have account?": "Vous avez un compte ?", "Have account?": "Vous avez un compte ?",
@ -558,6 +586,8 @@
"Bio": "Bio", "Bio": "Bio",
"Bio - Tooltip": "Bio - Infobulle", "Bio - Tooltip": "Bio - Infobulle",
"Cancel": "Abandonner", "Cancel": "Abandonner",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code envoyé", "Code Sent": "Code envoyé",
"Country/Region": "Pays/Région", "Country/Region": "Pays/Région",
"Country/Region - Tooltip": "Country/Region", "Country/Region - Tooltip": "Country/Region",

View File

@ -6,6 +6,9 @@
"Sign Up": "新規登録" "Sign Up": "新規登録"
}, },
"application": { "application": {
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
"Copy signup page URL": "Copy signup page URL",
"Edit Application": "アプリケーションを編集", "Edit Application": "アプリケーションを編集",
"Enable code signin": "コードサインインを有効にする", "Enable code signin": "コードサインインを有効にする",
"Enable code signin - Tooltip": "Enable code signin - Tooltip", "Enable code signin - Tooltip": "Enable code signin - Tooltip",
@ -19,21 +22,24 @@
"Password ON": "パスワードON", "Password ON": "パスワードON",
"Password ON - Tooltip": "Whether to allow password login", "Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "HTMLファイルを選択してください", "Please select a HTML file": "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", "Redirect URL": "リダイレクトURL",
"Redirect URLs": "リダイレクトURL", "Redirect URLs": "リダイレクトURL",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "トークンの更新の期限が切れます", "Refresh token expire": "トークンの更新の期限が切れます",
"Refresh token expire - Tooltip": "トークンの有効期限を更新する - ツールチップ", "Refresh token expire - Tooltip": "トークンの有効期限を更新する - ツールチップ",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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 that need to be filled in when users register", "Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "テストプロンプトページ...", "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",
"Test signin page..": "サインインテストページ...",
"Test signup page..": "登録ページのテスト",
"Token expire": "トークンの有効期限", "Token expire": "トークンの有効期限",
"Token expire - Tooltip": "トークンの有効期限 - ツールチップ", "Token expire - Tooltip": "トークンの有効期限 - ツールチップ",
"Token format": "トークンのフォーマット", "Token format": "トークンのフォーマット",
"Token format - Tooltip": "トークンフォーマット - ツールチップ" "Token format - Tooltip": "トークンフォーマット - ツールチップ",
"rule": "rule"
}, },
"cert": { "cert": {
"Bit size": "ビットサイズ", "Bit size": "ビットサイズ",
@ -99,6 +105,7 @@
"Cert": "Cert", "Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip", "Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs", "Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "クライアント IP", "Client IP": "クライアント IP",
"Created time": "作成日時", "Created time": "作成日時",
"Default avatar": "デフォルトのアバター", "Default avatar": "デフォルトのアバター",
@ -131,6 +138,9 @@
"Master password": "マスターパスワード", "Master password": "マスターパスワード",
"Master password - Tooltip": "マスターパスワード - ツールチップ", "Master password - Tooltip": "マスターパスワード - ツールチップ",
"Method": "方法", "Method": "方法",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "名前", "Name": "名前",
"Name - Tooltip": "Unique string-style identifier", "Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "OAuthプロバイダー", "OAuth providers": "OAuthプロバイダー",
@ -243,7 +253,15 @@
"sign up now": "今すぐサインアップ", "sign up now": "今すぐサインアップ",
"username, Email or phone": "ユーザー名、メールアドレスまたは電話番号" "username, Email or phone": "ユーザー名、メールアドレスまたは電話番号"
}, },
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": { "organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "デフォルトのアバター", "Default avatar": "デフォルトのアバター",
"Edit Organization": "組織を編集", "Edit Organization": "組織を編集",
"Favicon": "ファビコン", "Favicon": "ファビコン",
@ -255,7 +273,9 @@
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip", "Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL", "Website URL": "Website URL",
"Website URL - Tooltip": "Unique string-style identifier" "Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
}, },
"payment": { "payment": {
"Confirm your invoice information": "Confirm your invoice information", "Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +407,7 @@
"Domain": "ドメイン", "Domain": "ドメイン",
"Domain - Tooltip": "Storage endpoint custom domain", "Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "プロバイダーを編集", "Edit Provider": "プロバイダーを編集",
"Email Content": "Email Content", "Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier", "Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "メールタイトル", "Email Title": "メールタイトル",
"Email Title - Tooltip": "Unique string-style identifier", "Email Title - Tooltip": "Unique string-style identifier",
@ -425,7 +445,10 @@
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "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": "Send Test Email",
"Sign Name": "署名名", "Sign Name": "署名名",
"Sign Name - Tooltip": "Unique string-style identifier", "Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "サインリクエスト", "Sign request": "サインリクエスト",
@ -436,12 +459,17 @@
"Signup HTML": "HTMLの登録", "Signup HTML": "HTMLの登録",
"Signup HTML - Edit": "HTMLの登録 - 編集", "Signup HTML - Edit": "HTMLの登録 - 編集",
"Signup HTML - Tooltip": "サインアップ HTML - ツールチップ", "Signup HTML - Tooltip": "サインアップ HTML - ツールチップ",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type", "Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip", "Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "テンプレートコード", "Template Code": "テンプレートコード",
"Template Code - Tooltip": "Unique string-style identifier", "Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "利用規約", "Terms of Use": "利用規約",
"Terms of Use - Tooltip": "利用規約 - ツールチップ", "Terms of Use - Tooltip": "利用規約 - ツールチップ",
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"Token URL": "Token URL", "Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip", "Token URL - Tooltip": "Token URL - Tooltip",
"Type": "タイプ", "Type": "タイプ",
@ -454,7 +482,6 @@
"canUnlink": "canUnlink", "canUnlink": "canUnlink",
"prompted": "プロンプトされた", "prompted": "プロンプトされた",
"required": "必須", "required": "必須",
"rule": "ルール",
"visible": "表示" "visible": "表示"
}, },
"record": { "record": {
@ -483,6 +510,7 @@
}, },
"signup": { "signup": {
"Accept": "同意する", "Accept": "同意する",
"Agreement": "Agreement",
"Confirm": "確認する", "Confirm": "確認する",
"Decline": "同意しない", "Decline": "同意しない",
"Have account?": "アカウントをお持ちですか?", "Have account?": "アカウントをお持ちですか?",
@ -558,6 +586,8 @@
"Bio": "略歴", "Bio": "略歴",
"Bio - Tooltip": "バイオチップ(ツールチップ)", "Bio - Tooltip": "バイオチップ(ツールチップ)",
"Cancel": "キャンセル", "Cancel": "キャンセル",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "コードを送信しました", "Code Sent": "コードを送信しました",
"Country/Region": "国/地域", "Country/Region": "国/地域",
"Country/Region - Tooltip": "Country/Region", "Country/Region - Tooltip": "Country/Region",

View File

@ -6,6 +6,9 @@
"Sign Up": "Sign Up" "Sign Up": "Sign Up"
}, },
"application": { "application": {
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
"Copy signup page URL": "Copy signup page URL",
"Edit Application": "Edit Application", "Edit Application": "Edit Application",
"Enable code signin": "Enable code signin", "Enable code signin": "Enable code signin",
"Enable code signin - Tooltip": "Enable code signin - Tooltip", "Enable code signin - Tooltip": "Enable code signin - Tooltip",
@ -19,21 +22,24 @@
"Password ON": "Password ON", "Password ON": "Password ON",
"Password ON - Tooltip": "Whether to allow password login", "Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Please select a HTML file", "Please select a HTML file": "Please select a HTML file",
"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": "Redirect URL", "Redirect URL": "Redirect URL",
"Redirect URLs": "Redirect URLs", "Redirect URLs": "Redirect URLs",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Refresh token expire", "Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip", "Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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", "Signin session": "Signin session",
"Signup items": "Signup items", "Signup items": "Signup items",
"Signup items - Tooltip": "Signup items that need to be filled in when users register", "Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "Test prompt page..", "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",
"Test signin page..": "Test signin page..",
"Test signup page..": "Test signup page..",
"Token expire": "Token expire", "Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip", "Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip" "Token format - Tooltip": "Token format - Tooltip",
"rule": "rule"
}, },
"cert": { "cert": {
"Bit size": "Bit size", "Bit size": "Bit size",
@ -99,6 +105,7 @@
"Cert": "Cert", "Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip", "Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs", "Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "Client IP", "Client IP": "Client IP",
"Created time": "Created time", "Created time": "Created time",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -131,6 +138,9 @@
"Master password": "Master password", "Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip", "Master password - Tooltip": "Master password - Tooltip",
"Method": "Method", "Method": "Method",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Name", "Name": "Name",
"Name - Tooltip": "Unique string-style identifier", "Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "OAuth providers", "OAuth providers": "OAuth providers",
@ -243,7 +253,15 @@
"sign up now": "sign up now", "sign up now": "sign up now",
"username, Email or phone": "username, Email or phone" "username, Email or phone": "username, Email or phone"
}, },
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": { "organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
"Edit Organization": "Edit Organization", "Edit Organization": "Edit Organization",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -255,7 +273,9 @@
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip", "Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL", "Website URL": "Website URL",
"Website URL - Tooltip": "Unique string-style identifier" "Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
}, },
"payment": { "payment": {
"Confirm your invoice information": "Confirm your invoice information", "Confirm your invoice information": "Confirm your invoice information",
@ -387,9 +407,9 @@
"Domain": "Domain", "Domain": "Domain",
"Domain - Tooltip": "Storage endpoint custom domain", "Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "Edit Provider", "Edit Provider": "Edit Provider",
"Email Content": "Email Content", "Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier", "Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "Email Title", "Email Title": "Email title",
"Email Title - Tooltip": "Unique string-style identifier", "Email Title - Tooltip": "Unique string-style identifier",
"Endpoint": "Endpoint", "Endpoint": "Endpoint",
"Endpoint (Intranet)": "Endpoint (Intranet)", "Endpoint (Intranet)": "Endpoint (Intranet)",
@ -425,7 +445,10 @@
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Secret access key", "Secret access key": "Secret access key",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip", "SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
"Send Test Email": "Send Test Email",
"Sign Name": "Sign Name", "Sign Name": "Sign Name",
"Sign Name - Tooltip": "Unique string-style identifier", "Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Sign request", "Sign request": "Sign request",
@ -436,12 +459,17 @@
"Signup HTML": "Signup HTML", "Signup HTML": "Signup HTML",
"Signup HTML - Edit": "Signup HTML - Edit", "Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Signup HTML - Tooltip", "Signup HTML - Tooltip": "Signup HTML - Tooltip",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type", "Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip", "Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Template Code", "Template Code": "Template Code",
"Template Code - Tooltip": "Unique string-style identifier", "Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Terms of Use", "Terms of Use": "Terms of Use",
"Terms of Use - Tooltip": "Terms of Use - Tooltip", "Terms of Use - Tooltip": "Terms of Use - Tooltip",
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"Token URL": "Token URL", "Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip", "Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Type", "Type": "Type",
@ -454,7 +482,6 @@
"canUnlink": "canUnlink", "canUnlink": "canUnlink",
"prompted": "prompted", "prompted": "prompted",
"required": "required", "required": "required",
"rule": "rule",
"visible": "visible" "visible": "visible"
}, },
"record": { "record": {
@ -483,6 +510,7 @@
}, },
"signup": { "signup": {
"Accept": "Accept", "Accept": "Accept",
"Agreement": "Agreement",
"Confirm": "Confirm", "Confirm": "Confirm",
"Decline": "Decline", "Decline": "Decline",
"Have account?": "Have account?", "Have account?": "Have account?",
@ -558,6 +586,8 @@
"Bio": "Bio", "Bio": "Bio",
"Bio - Tooltip": "Bio - Tooltip", "Bio - Tooltip": "Bio - Tooltip",
"Cancel": "Cancel", "Cancel": "Cancel",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code Sent", "Code Sent": "Code Sent",
"Country/Region": "Country/Region", "Country/Region": "Country/Region",
"Country/Region - Tooltip": "Country/Region", "Country/Region - Tooltip": "Country/Region",

View File

@ -6,6 +6,9 @@
"Sign Up": "Регистрация" "Sign Up": "Регистрация"
}, },
"application": { "application": {
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
"Copy signup page URL": "Copy signup page URL",
"Edit Application": "Изменить приложение", "Edit Application": "Изменить приложение",
"Enable code signin": "Включить кодовый вход", "Enable code signin": "Включить кодовый вход",
"Enable code signin - Tooltip": "Включить вход с кодом - Tooltip", "Enable code signin - Tooltip": "Включить вход с кодом - Tooltip",
@ -19,21 +22,24 @@
"Password ON": "Пароль ВКЛ", "Password ON": "Пароль ВКЛ",
"Password ON - Tooltip": "Whether to allow password login", "Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Пожалуйста, выберите HTML-файл", "Please select a HTML file": "Пожалуйста, выберите 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 перенаправления", "Redirect URL": "URL перенаправления",
"Redirect URLs": "Перенаправление URL", "Redirect URLs": "Перенаправление URL",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Срок действия обновления токена истекает", "Refresh token expire": "Срок действия обновления токена истекает",
"Refresh token expire - Tooltip": "Срок обновления токена истекает - Подсказка", "Refresh token expire - Tooltip": "Срок обновления токена истекает - Подсказка",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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 that need to be filled in when users register", "Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "Тестовая страница запроса..", "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",
"Test signin page..": "Тестовая страница входа..",
"Test signup page..": "Тестовая страница регистрации..",
"Token expire": "Токен истекает", "Token expire": "Токен истекает",
"Token expire - Tooltip": "Истек токен - Подсказка", "Token expire - Tooltip": "Истек токен - Подсказка",
"Token format": "Формат токена", "Token format": "Формат токена",
"Token format - Tooltip": "Формат токена - Подсказка" "Token format - Tooltip": "Формат токена - Подсказка",
"rule": "rule"
}, },
"cert": { "cert": {
"Bit size": "Размер бита", "Bit size": "Размер бита",
@ -99,6 +105,7 @@
"Cert": "Cert", "Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip", "Cert - Tooltip": "Cert - Tooltip",
"Certs": "Сертификаты", "Certs": "Сертификаты",
"Click to Upload": "Click to Upload",
"Client IP": "IP клиента", "Client IP": "IP клиента",
"Created time": "Время создания", "Created time": "Время создания",
"Default avatar": "Аватар по умолчанию", "Default avatar": "Аватар по умолчанию",
@ -131,6 +138,9 @@
"Master password": "Мастер-пароль", "Master password": "Мастер-пароль",
"Master password - Tooltip": "Мастер-пароль - Tooltip", "Master password - Tooltip": "Мастер-пароль - Tooltip",
"Method": "Метод", "Method": "Метод",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Наименование", "Name": "Наименование",
"Name - Tooltip": "Unique string-style identifier", "Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "Поставщики OAuth", "OAuth providers": "Поставщики OAuth",
@ -243,7 +253,15 @@
"sign up now": "зарегистрироваться", "sign up now": "зарегистрироваться",
"username, Email or phone": "имя пользователя, адрес электронной почты или телефон" "username, Email or phone": "имя пользователя, адрес электронной почты или телефон"
}, },
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": { "organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Аватар по умолчанию", "Default avatar": "Аватар по умолчанию",
"Edit Organization": "Изменить организацию", "Edit Organization": "Изменить организацию",
"Favicon": "Иконка", "Favicon": "Иконка",
@ -255,7 +273,9 @@
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip", "Tags - Tooltip": "Tags - Tooltip",
"Website URL": "URL сайта", "Website URL": "URL сайта",
"Website URL - Tooltip": "Unique string-style identifier" "Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
}, },
"payment": { "payment": {
"Confirm your invoice information": "Confirm your invoice information", "Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +407,7 @@
"Domain": "Домен", "Domain": "Домен",
"Domain - Tooltip": "Storage endpoint custom domain", "Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "Изменить провайдера", "Edit Provider": "Изменить провайдера",
"Email Content": "Email Content", "Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier", "Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "Заголовок письма", "Email Title": "Заголовок письма",
"Email Title - Tooltip": "Unique string-style identifier", "Email Title - Tooltip": "Unique string-style identifier",
@ -425,7 +445,10 @@
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Секретный ключ доступа", "Secret access key": "Секретный ключ доступа",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Подсказка", "SecretAccessKey - Tooltip": "SecretAccessKey - Подсказка",
"Send Test Email": "Send Test Email",
"Sign Name": "Имя подписи", "Sign Name": "Имя подписи",
"Sign Name - Tooltip": "Unique string-style identifier", "Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Запрос на подпись", "Sign request": "Запрос на подпись",
@ -436,12 +459,17 @@
"Signup HTML": "Регистрация HTML", "Signup HTML": "Регистрация HTML",
"Signup HTML - Edit": "Регистрация HTML - Редактировать", "Signup HTML - Edit": "Регистрация HTML - Редактировать",
"Signup HTML - Tooltip": "Регистрация HTML - Подсказка", "Signup HTML - Tooltip": "Регистрация HTML - Подсказка",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type", "Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip", "Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Код шаблона", "Template Code": "Код шаблона",
"Template Code - Tooltip": "Unique string-style identifier", "Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Условия использования", "Terms of Use": "Условия использования",
"Terms of Use - Tooltip": "Условия использования - Tooltip", "Terms of Use - Tooltip": "Условия использования - Tooltip",
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"Token URL": "Token URL", "Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip", "Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Тип", "Type": "Тип",
@ -454,7 +482,6 @@
"canUnlink": "canUnlink", "canUnlink": "canUnlink",
"prompted": "запрошено", "prompted": "запрошено",
"required": "обязательный", "required": "обязательный",
"rule": "правило",
"visible": "видимый" "visible": "видимый"
}, },
"record": { "record": {
@ -483,6 +510,7 @@
}, },
"signup": { "signup": {
"Accept": "Принять", "Accept": "Принять",
"Agreement": "Agreement",
"Confirm": "Подтвердить", "Confirm": "Подтвердить",
"Decline": "Отклонить", "Decline": "Отклонить",
"Have account?": "Есть аккаунт?", "Have account?": "Есть аккаунт?",
@ -558,6 +586,8 @@
"Bio": "Био", "Bio": "Био",
"Bio - Tooltip": "Био - Подсказка", "Bio - Tooltip": "Био - Подсказка",
"Cancel": "Отмена", "Cancel": "Отмена",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Код отправлен", "Code Sent": "Код отправлен",
"Country/Region": "Страна/регион", "Country/Region": "Страна/регион",
"Country/Region - Tooltip": "Country/Region", "Country/Region - Tooltip": "Country/Region",

View File

@ -6,6 +6,9 @@
"Sign Up": "注册" "Sign Up": "注册"
}, },
"application": { "application": {
"Copy prompt page URL": "复制提醒页面URL",
"Copy signin page URL": "复制登录页面URL",
"Copy signup page URL": "复制注册页面URL",
"Edit Application": "编辑应用", "Edit Application": "编辑应用",
"Enable code signin": "启用验证码登录", "Enable code signin": "启用验证码登录",
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录", "Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
@ -19,21 +22,24 @@
"Password ON": "开启密码", "Password ON": "开启密码",
"Password ON - Tooltip": "是否允许密码登录", "Password ON - Tooltip": "是否允许密码登录",
"Please select a HTML file": "请选择一个HTML文件", "Please select a HTML file": "请选择一个HTML文件",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "提醒页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Redirect URL": "重定向 URL", "Redirect URL": "重定向 URL",
"Redirect URLs": "重定向 URLs", "Redirect URLs": "重定向 URLs",
"Redirect URLs - Tooltip": "登录成功后重定向地址列表", "Redirect URLs - Tooltip": "登录成功后重定向地址列表",
"Refresh token expire": "Refresh Token过期时间", "Refresh token expire": "Refresh Token过期",
"Refresh token expire - Tooltip": "Refresh Token过期时间", "Refresh token expire - Tooltip": "Refresh Token过期时间",
"SAML metadata": "SAML元数据",
"SAML metadata - Tooltip": "SAML协议的元数据Metadata信息",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "登录页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Signin session": "保持登录会话", "Signin session": "保持登录会话",
"Signup items": "注册项", "Signup items": "注册项",
"Signup items - Tooltip": "注册用户注册时需要填写的项目", "Signup items - Tooltip": "注册用户注册时需要填写的项目",
"Test prompt page..": "测试提醒页面..", "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "注册页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Test signin page..": "测试登录页面..", "Token expire": "Access Token过期",
"Test signup page..": "测试注册页面..",
"Token expire": "Access Token过期时间",
"Token expire - Tooltip": "Access Token过期时间", "Token expire - Tooltip": "Access Token过期时间",
"Token format": "Access Token格式", "Token format": "Access Token格式",
"Token format - Tooltip": "Access Token格式" "Token format - Tooltip": "Access Token格式",
"rule": "规则"
}, },
"cert": { "cert": {
"Bit size": "位大小", "Bit size": "位大小",
@ -99,6 +105,7 @@
"Cert": "证书", "Cert": "证书",
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书", "Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
"Certs": "证书", "Certs": "证书",
"Click to Upload": "点击上传",
"Client IP": "客户端IP", "Client IP": "客户端IP",
"Created time": "创建时间", "Created time": "创建时间",
"Default avatar": "默认头像", "Default avatar": "默认头像",
@ -131,6 +138,9 @@
"Master password": "万能密码", "Master password": "万能密码",
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题", "Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
"Method": "方法", "Method": "方法",
"Model": "模型",
"Model - Tooltip": "Casbin模型",
"Models": "模型",
"Name": "名称", "Name": "名称",
"Name - Tooltip": "唯一的、字符串式的ID", "Name - Tooltip": "唯一的、字符串式的ID",
"OAuth providers": "OAuth提供方", "OAuth providers": "OAuth提供方",
@ -243,11 +253,19 @@
"sign up now": "立即注册", "sign up now": "立即注册",
"username, Email or phone": "用户名、Email或手机号" "username, Email or phone": "用户名、Email或手机号"
}, },
"model": {
"Edit Model": "编辑模型",
"Model text": "模型文本",
"Model text - Tooltip": "Casbin访问控制模型",
"New Model": "添加模型"
},
"organization": { "organization": {
"Account items": "个人页设置项",
"Account items - Tooltip": "个人设置页面中的项目",
"Default avatar": "默认头像", "Default avatar": "默认头像",
"Edit Organization": "编辑组织", "Edit Organization": "编辑组织",
"Favicon": "图标", "Favicon": "图标",
"Is profile public": "公开用户主页", "Is profile public": "用户个人页公开",
"Is profile public - Tooltip": "关闭后,只有全局管理员或同组织用户才能访问用户主页", "Is profile public - Tooltip": "关闭后,只有全局管理员或同组织用户才能访问用户主页",
"New Organization": "添加组织", "New Organization": "添加组织",
"Soft deletion": "软删除", "Soft deletion": "软删除",
@ -255,7 +273,9 @@
"Tags": "标签集合", "Tags": "标签集合",
"Tags - Tooltip": "可供用户选择的标签的集合", "Tags - Tooltip": "可供用户选择的标签的集合",
"Website URL": "网页地址", "Website URL": "网页地址",
"Website URL - Tooltip": "网页地址" "Website URL - Tooltip": "网页地址",
"modifyRule": "修改规则",
"viewRule": "查看规则"
}, },
"payment": { "payment": {
"Confirm your invoice information": "确认您的发票信息", "Confirm your invoice information": "确认您的发票信息",
@ -399,7 +419,7 @@
"IdP public key": "IdP 公钥", "IdP public key": "IdP 公钥",
"Issuer URL": "发行者网址", "Issuer URL": "发行者网址",
"Issuer URL - Tooltip": "发行者URL - 工具提示", "Issuer URL - Tooltip": "发行者URL - 工具提示",
"Link copied to clipboard successfully": "链接复制到剪贴板成功", "Link copied to clipboard successfully": "链接已成功复制到剪贴板",
"Metadata": "元数据", "Metadata": "元数据",
"Metadata - Tooltip": "元数据 - 工具提示", "Metadata - Tooltip": "元数据 - 工具提示",
"Method": "方法", "Method": "方法",
@ -425,23 +445,31 @@
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - 工具提示", "Scope - Tooltip": "Scope - 工具提示",
"Secret access key": "秘密访问密钥", "Secret access key": "秘密访问密钥",
"Secret key": "Secret key",
"Secret key - Tooltip": "用于服务端调用验证码提供商API进行验证",
"SecretAccessKey - Tooltip": "访问密钥-工具提示", "SecretAccessKey - Tooltip": "访问密钥-工具提示",
"Send Test Email": "发送测试邮件",
"Sign Name": "签名名称", "Sign Name": "签名名称",
"Sign Name - Tooltip": "签名名称", "Sign Name - Tooltip": "签名名称",
"Sign request": "签名请求", "Sign request": "签名请求",
"Sign request - Tooltip": "签名请求 - 工具提示", "Sign request - Tooltip": "签名请求 - 工具提示",
"Signin HTML": "登录 HTML", "Signin HTML": "登录页面HTML",
"Signin HTML - Edit": "登录 HTML - 编辑", "Signin HTML - Edit": "登录页面 - 编辑",
"Signin HTML - Tooltip": "登录 HTML - 工具提示", "Signin HTML - Tooltip": "自定义HTML,用于替换默认的登录页面样式",
"Signup HTML": "注册 HTML", "Signup HTML": "注册页面HTML",
"Signup HTML - Edit": "注册 HTML - 编辑", "Signup HTML - Edit": "注册页面HTML - 编辑",
"Signup HTML - Tooltip": "注册 HTML - 工具提示", "Signup HTML - Tooltip": "自定义HTML,用于替换默认的注册页面样式",
"Site key": "Site key",
"Site key - Tooltip": "用于前端嵌入页面",
"Sub type": "子类型", "Sub type": "子类型",
"Sub type - Tooltip": "子类型", "Sub type - Tooltip": "子类型",
"Template Code": "模板CODE", "Template Code": "模板代码",
"Template Code - Tooltip": "模板CODE", "Template Code - Tooltip": "模板代码",
"Terms of Use": "使用条款", "Terms of Use": "使用条款",
"Terms of Use - Tooltip": "使用条款 - 工具提示", "Terms of Use - Tooltip": "使用条款 - 工具提示",
"Test Connection": "测试Smtp连接",
"Test Email": "测试Email配置",
"Test Email - Tooltip": "邮箱地址",
"Token URL": "Token URL", "Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - 工具提示", "Token URL - Tooltip": "Token URL - 工具提示",
"Type": "类型", "Type": "类型",
@ -449,13 +477,12 @@
"UserInfo URL": "UserInfo URL", "UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - 工具提示", "UserInfo URL - Tooltip": "UserInfo URL - 工具提示",
"alertType": "警报类型", "alertType": "警报类型",
"canSignIn": "canSignIn", "canSignIn": "可用于登录",
"canSignUp": "canSignUp", "canSignUp": "可用于注册",
"canUnlink": "canUnlink", "canUnlink": "可解绑定",
"prompted": "提示", "prompted": "注册后提醒绑定",
"required": "必需", "required": "是否必填项",
"rule": "规则", "visible": "是否可见"
"visible": "可见"
}, },
"record": { "record": {
"Is Triggered": "已触发" "Is Triggered": "已触发"
@ -483,6 +510,7 @@
}, },
"signup": { "signup": {
"Accept": "阅读并接受", "Accept": "阅读并接受",
"Agreement": "用户协议",
"Confirm": "确认密码", "Confirm": "确认密码",
"Decline": "不接受", "Decline": "不接受",
"Have account?": "已有账号?", "Have account?": "已有账号?",
@ -558,6 +586,8 @@
"Bio": "自我介绍", "Bio": "自我介绍",
"Bio - Tooltip": "自我介绍", "Bio - Tooltip": "自我介绍",
"Cancel": "取消", "Cancel": "取消",
"Captcha Verify Failed": "验证码校验失败",
"Captcha Verify Success": "验证码校验成功",
"Code Sent": "验证码已发送", "Code Sent": "验证码已发送",
"Country/Region": "国家/地区", "Country/Region": "国家/地区",
"Country/Region - Tooltip": "国家/地区", "Country/Region - Tooltip": "国家/地区",