Compare commits

..

44 Commits

Author SHA1 Message Date
cf4e76f9dc feat: add footer to door pages (#868) 2022-07-08 20:36:49 +08:00
81f2d01dc1 fix: fix dockerfile (#866) 2022-07-07 16:10:15 +08:00
61773d3173 fix: support user-defined clientId&Secret (#862) 2022-07-06 19:27:59 +08:00
ec29621547 feat: init from configuration file (#858)
* feat: init from configuration file

* Update init_data.json.template

* Update init_data.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-05 00:36:22 +08:00
b8e324cadf fix: azurad provider (#855) 2022-07-04 16:40:23 +08:00
f37fd6ba87 Fix empty arg bug in getPermanentAvatarUrl(). 2022-07-03 19:31:12 +08:00
b4bf734fe8 fix: fix cors filter (#847)
* fix: fix cors filter

* Update cors_filter.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-02 13:45:18 +08:00
f0431701c9 fix: fix OAuth error response (#835)
* fix: fix OAuth error response

* fix: provide more detailed error messages for TokenError
2022-07-01 14:53:34 +08:00
aa5078de15 fix: crowdin kept deleting translations (#843)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-01 10:51:40 +08:00
9a324b2cca fix: Update Crowdin link (#841) 2022-06-30 22:05:20 +08:00
919eaf1df4 fix: fix CORS error after sucessful OPTION (#838) 2022-06-30 21:29:02 +08:00
cd902a21ba fix: some minor bugs and make Dockerfile more productive. (#831)
* fix: some minor bugs and make Dockerfile more productive.

* fix: make GitHub CI configuration support build image with STANDARD target.

* fix: Naming the base stage in multi-stage builds with lowercase letters to support various operating systems.

* fix: copy swagger to the image as well.
2022-06-29 23:21:18 +08:00
fe0ab0aa6f Fix downloadFile()'s google proxy. 2022-06-29 22:01:38 +08:00
a0e11cc8a0 feat: add aliyun captcha (#833)
* feat: add aliyun captcha provider

* Rename App key

* fix typo

* Rename HMACSHA1 & Reused clientId2 and clientSecret2

* Update ProviderEditPage.js

* Delete unused import

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-29 11:31:32 +08:00
8a66448365 feat: support casdoor as saml idp to connect keycloak (#832)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-06-28 22:05:02 +08:00
477d386f3c fix: captcha preview panic when clientId or clientSecret is empty (#824)
* fix: captcha preview panic when clientId or clientSecret is empty

* return original errors from captcha
2022-06-26 22:09:57 +08:00
339c6c2dd0 Fix null bug in getTermsofuseContent(). 2022-06-26 09:34:01 +08:00
7c9370ef90 feat: add CORS filter to fix OPTION request failure (#826) 2022-06-26 01:28:33 +08:00
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
78 changed files with 3987 additions and 778 deletions

View File

@ -114,6 +114,7 @@ jobs:
uses: docker/build-push-action@v2
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
with:
target: STANDARD
push: true
tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest

View File

@ -1,8 +1,3 @@
FROM golang:1.17.5 AS BACK
WORKDIR /go/src/casdoor
COPY . .
RUN ./build.sh && apt update && apt install wait-for-it && chmod +x /usr/bin/wait-for-it
FROM node:16.13.0 AS FRONT
WORKDIR /web
COPY ./web .
@ -10,28 +5,42 @@ RUN yarn config set registry https://registry.npmmirror.com
RUN yarn install && yarn run build
FROM debian:latest AS ALLINONE
RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates
RUN apt install -y mariadb-server mariadb-client && mkdir -p web/build && chmod 777 /tmp
LABEL MAINTAINER="https://casdoor.org/"
COPY --from=BACK /go/src/casdoor/ ./
COPY --from=BACK /usr/bin/wait-for-it ./
COPY --from=FRONT /web/build /web/build
CMD chmod 777 /tmp && service mariadb start&&\
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ; fi&&\
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD} &&\
./wait-for-it localhost:3306 -- ./server --createDatabase=true
FROM golang:1.17.5 AS BACK
WORKDIR /go/src/casdoor
COPY . .
RUN ./build.sh
FROM alpine:latest
RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add curl
RUN apk add ca-certificates && update-ca-certificates
FROM alpine:latest AS STANDARD
LABEL MAINTAINER="https://casdoor.org/"
COPY --from=BACK /go/src/casdoor/ ./
COPY --from=BACK /usr/bin/wait-for-it ./
RUN mkdir -p web/build && apk add --no-cache bash coreutils
COPY --from=FRONT /web/build /web/build
CMD ./server
WORKDIR /
COPY --from=BACK /go/src/casdoor/server ./server
COPY --from=BACK /go/src/casdoor/swagger ./swagger
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf
COPY --from=FRONT /web/build ./web/build
ENTRYPOINT ["/server"]
FROM debian:latest AS db
RUN apt update \
&& apt install -y \
mariadb-server \
mariadb-client \
&& rm -rf /var/lib/apt/lists/*
FROM db AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/"
ENV MYSQL_ROOT_PASSWORD=123456
WORKDIR /
COPY --from=BACK /go/src/casdoor/server ./server
COPY --from=BACK /go/src/casdoor/swagger ./swagger
COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf
COPY --from=FRONT /web/build ./web/build
ENTRYPOINT ["/bin/bash"]
CMD ["/docker-entrypoint.sh"]

View File

@ -98,7 +98,7 @@ For casdoor, if you have any questions, you can give Issues, or you can also dir
### 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-site) as translating platform and i18next as translating tool. When you add some words using i18next in the ```web/``` directory, please remember to add what you have added to the ```web/src/locales/en/data.json``` file.

View File

@ -95,7 +95,8 @@ p, *, *, GET, /api/get-providers, *, *
p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, *
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/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, *

105
captcha/aliyun.go Normal file
View File

@ -0,0 +1,105 @@
// 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"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/casdoor/casdoor/util"
)
const AliyunCaptchaVerifyUrl = "http://afs.aliyuncs.com"
type AliyunCaptchaProvider struct {
}
func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
captcha := &AliyunCaptchaProvider{}
return captcha
}
func contentEscape(str string) string {
str = strings.Replace(str, " ", "%20", -1)
str = url.QueryEscape(str)
return str
}
func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
pathData, err := url.ParseQuery(token)
if err != nil {
return false, err
}
pathData["Action"] = []string{"AuthenticateSig"}
pathData["Format"] = []string{"json"}
pathData["SignatureMethod"] = []string{"HMAC-SHA1"}
pathData["SignatureNonce"] = []string{strconv.FormatInt(time.Now().UnixNano(), 10)}
pathData["SignatureVersion"] = []string{"1.0"}
pathData["Timestamp"] = []string{time.Now().UTC().Format("2006-01-02T15:04:05Z")}
pathData["Version"] = []string{"2018-01-12"}
var keys []string
for k := range pathData {
keys = append(keys, k)
}
sort.Strings(keys)
sortQuery := ""
for _, k := range keys {
sortQuery += k + "=" + contentEscape(pathData[k][0]) + "&"
}
sortQuery = strings.TrimSuffix(sortQuery, "&")
stringToSign := fmt.Sprintf("GET&%s&%s", url.QueryEscape("/"), url.QueryEscape(sortQuery))
signature := util.GetHmacSha1(clientSecret+"&", stringToSign)
resp, err := http.Get(fmt.Sprintf("%s?%s&Signature=%s", AliyunCaptchaVerifyUrl, sortQuery, url.QueryEscape(signature)))
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 {
Code int `json:"Code"`
Msg string `json:"Msg"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
if captchaResp.Code != 100 {
return false, errors.New(captchaResp.Msg)
}
return true, nil
}

29
captcha/default.go Normal file
View File

@ -0,0 +1,29 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package captcha
import "github.com/casdoor/casdoor/object"
type DefaultCaptchaProvider struct {
}
func NewDefaultCaptchaProvider() *DefaultCaptchaProvider {
captcha := &DefaultCaptchaProvider{}
return captcha
}
func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
return object.VerifyCaptcha(clientSecret, token), nil
}

67
captcha/hcaptcha.go Normal file
View File

@ -0,0 +1,67 @@
// 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"
"errors"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
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"`
ErrorCodes []string `json:"error-codes"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
if len(captchaResp.ErrorCodes) > 0 {
return false, errors.New(strings.Join(captchaResp.ErrorCodes, ","))
}
return captchaResp.Success, nil
}

32
captcha/provider.go Normal file
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.
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()
} else if captchaType == "Aliyun Captcha" {
return NewAliyunCaptchaProvider()
}
return nil
}

67
captcha/recaptcha.go Normal file
View File

@ -0,0 +1,67 @@
// 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"
"errors"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
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"`
ErrorCodes []string `json:"error-codes"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
if len(captchaResp.ErrorCodes) > 0 {
return false, errors.New(strings.Join(captchaResp.ErrorCodes, ","))
}
return captchaResp.Success, nil
}

View File

@ -17,6 +17,7 @@ package conf
import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
@ -61,7 +62,12 @@ func GetBeegoConfDataSourceName() string {
runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
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

View File

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

View File

@ -133,7 +133,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
// @Param redirectUri query string true "redirect uri"
// @Param scope query string true "scope"
// @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]
func (c *ApiController) GetApplicationLogin() {
clientId := c.Input().Get("clientId")
@ -163,9 +163,16 @@ func setHttpClient(idProvider idp.IdProvider, providerType string) {
// @Title Login
// @Tag Login API
// @Description login
// @Param oAuthParams query string true "oAuth parameters"
// @Param body body RequestForm true "Login information"
// @Success 200 {object} controllers.api_controller.Response The Response object
// @Param clientId query string true clientId
// @Param responseType query string true responseType
// @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]
func (c *ApiController) Login() {
resp := &Response{}

View File

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

View File

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

View File

@ -25,33 +25,61 @@ import (
"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
// @Title SendEmail
// @Tag Service API
// @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 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
// @router /api/send-email [post]
func (c *ApiController) SendEmail() {
provider, _, ok := c.GetProviderFromContext("Email")
if !ok {
return
}
var emailForm EmailForm
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)
if err != nil {
c.ResponseError(err.Error())
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) {
c.ResponseError(fmt.Sprintf("Empty parameters for emailForm: %v", emailForm))
return
@ -86,7 +114,7 @@ func (c *ApiController) SendEmail() {
// @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 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
// @router /api/send-sms [post]
func (c *ApiController) SendSms() {
@ -95,11 +123,7 @@ func (c *ApiController) SendSms() {
return
}
var smsForm struct {
Content string `json:"content"`
Receivers []string `json:"receivers"`
OrgId string `json:"organizationId"` // e.g. "admin/built-in"
}
var smsForm SmsForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm)
if err != nil {
c.ResponseError(err.Error())

View File

@ -165,6 +165,8 @@ func (c *ApiController) GetOAuthCode() {
// @Param client_secret query string true "OAuth client secret"
// @Param code query string true "OAuth code"
// @Success 200 {object} object.TokenWrapper The Response object
// @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object
// @router /login/oauth/access_token [post]
func (c *ApiController) GetOAuthToken() {
grantType := c.Input().Get("grant_type")
@ -200,6 +202,7 @@ func (c *ApiController) GetOAuthToken() {
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, tag, avatar)
c.SetTokenErrorHttpStatus()
c.ServeJSON()
}
@ -213,6 +216,8 @@ func (c *ApiController) GetOAuthToken() {
// @Param client_id query string true "OAuth client id"
// @Param client_secret query string false "OAuth client secret"
// @Success 200 {object} object.TokenWrapper The Response object
// @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object
// @router /login/oauth/refresh_token [post]
func (c *ApiController) RefreshToken() {
grantType := c.Input().Get("grant_type")
@ -235,6 +240,7 @@ func (c *ApiController) RefreshToken() {
}
c.Data["json"] = object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
c.SetTokenErrorHttpStatus()
c.ServeJSON()
}
@ -270,6 +276,8 @@ func (c *ApiController) TokenLogout() {
// @Param token formData string true "access_token's value or refresh_token's value"
// @Param token_type_hint formData string true "the token type access_token or refresh_token"
// @Success 200 {object} object.IntrospectionResponse The Response object
// @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object
// @router /login/oauth/introspect [post]
func (c *ApiController) IntrospectToken() {
tokenValue := c.Input().Get("token")
@ -279,12 +287,21 @@ func (c *ApiController) IntrospectToken() {
clientSecret = c.Input().Get("client_secret")
if clientId == "" || clientSecret == "" {
c.ResponseError("empty clientId or clientSecret")
c.Data["json"] = &object.TokenError{
Error: object.INVALID_REQUEST,
}
c.SetTokenErrorHttpStatus()
c.ServeJSON()
return
}
}
application := object.GetApplicationByClientId(clientId)
if application == nil || application.ClientSecret != clientSecret {
c.ResponseError("invalid application or wrong clientSecret")
c.Data["json"] = &object.TokenError{
Error: object.INVALID_CLIENT,
}
c.SetTokenErrorHttpStatus()
return
}
token := object.GetTokenByTokenAndApplication(tokenValue, application.Name)

View File

@ -51,6 +51,23 @@ func (c *ApiController) ResponseError(error string, data ...interface{}) {
c.ServeJSON()
}
// SetTokenErrorHttpStatus ...
func (c *ApiController) SetTokenErrorHttpStatus() {
_, ok := c.Data["json"].(*object.TokenError)
if ok {
if c.Data["json"].(*object.TokenError).Error == object.INVALID_CLIENT {
c.Ctx.Output.SetStatus(401)
c.Ctx.Output.Header("WWW-Authenticate", "Basic realm=\"OAuth2\"")
} else {
c.Ctx.Output.SetStatus(400)
}
}
_, ok = c.Data["json"].(*object.TokenWrapper)
if ok {
c.Ctx.Output.SetStatus(200)
}
}
// RequireSignedIn ...
func (c *ApiController) RequireSignedIn() (string, bool) {
userId := c.GetSessionUsername()

View File

@ -19,6 +19,7 @@ import (
"fmt"
"strings"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@ -48,20 +49,28 @@ func (c *ApiController) SendVerificationCode() {
checkUser := c.Ctx.Request.Form.Get("checkUser")
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.")
return
}
isHuman := false
captchaProvider := object.GetDefaultHumanCheckProvider()
if captchaProvider == nil {
isHuman = object.VerifyCaptcha(checkId, checkKey)
}
captchaProvider := captcha.GetCaptchaProvider(checkType)
if !isHuman {
c.ResponseError("Turing test failed.")
return
if captchaProvider != nil {
if checkKey == "" {
c.ResponseError("Missing parameter: checkKey.")
return
}
isHuman, err := captchaProvider.VerifyCaptcha(checkKey, checkId)
if err != nil {
c.ResponseError(err.Error())
return
}
if !isHuman {
c.ResponseError("Turing test failed.")
return
}
}
user := c.getCurrentUser()
@ -173,3 +182,36 @@ func (c *ApiController) ResetEmailOrPhone() {
c.Data["json"] = Response{Status: "ok"}
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(err.Error())
return
}
c.ResponseOk(isValid)
}

View File

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

View File

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

View File

@ -25,3 +25,10 @@ func TestGetSaltedPassword(t *testing.T) {
cm := NewSha256SaltCredManager()
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

@ -5,6 +5,7 @@ services:
build:
context: ./
dockerfile: Dockerfile
target: STANDARD
entrypoint: /bin/sh -c './server --createDatabase=true'
ports:
- "8000:8000"
@ -12,8 +13,6 @@ services:
- db
environment:
RUNNING_IN_DOCKER: "true"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./conf:/conf/
db:

7
docker-entrypoint.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
service mariadb start
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
exec /server --createDatabase=true

2
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/casbin/casbin/v2 v2.30.1
github.com/casbin/xorm-adapter/v2 v2.5.1
github.com/casdoor/go-sms-sender v0.2.0
github.com/casdoor/goth v1.69.0-FIX1
github.com/casdoor/goth v1.69.0-FIX2
github.com/casdoor/oss v1.2.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df

4
go.sum
View File

@ -100,8 +100,8 @@ github.com/casbin/xorm-adapter/v2 v2.5.1 h1:BkpIxRHKa0s3bSMx173PpuU7oTs+Zw7XmD0B
github.com/casbin/xorm-adapter/v2 v2.5.1/go.mod h1:AeH4dBKHC9/zYxzdPVHhPDzF8LYLqjDdb767CWJoV54=
github.com/casdoor/go-sms-sender v0.2.0 h1:52bin4EBOPzOee64s9UK7jxd22FODvT9/+Y/Z+PSHpg=
github.com/casdoor/go-sms-sender v0.2.0/go.mod h1:fsZsNnALvFIo+HFcE1U/oCQv4ZT42FdglXKMsEm3WSk=
github.com/casdoor/goth v1.69.0-FIX1 h1:24Y3tfaJxWGJbxickGe3F9y2c8X1PgsQynhxGXV1f9Q=
github.com/casdoor/goth v1.69.0-FIX1/go.mod h1:Om55nRo8CkeDkPSNBbzXW4G5uI28ZUkSk5S69dPek3s=
github.com/casdoor/goth v1.69.0-FIX2 h1:RgfIMkL9kekylgxHHK2ZY8ASAwOGns2HVlaBwLu7Bcs=
github.com/casdoor/goth v1.69.0-FIX2/go.mod h1:Om55nRo8CkeDkPSNBbzXW4G5uI28ZUkSk5S69dPek3s=
github.com/casdoor/oss v1.2.0 h1:ozLAE+nnNdFQBWbzH8U9spzaO8h8NrB57lBcdyMUUQ8=
github.com/casdoor/oss v1.2.0/go.mod h1:qii35VBuxnR/uEuYSKpS0aJ8htQFOcCVsZ4FHgHLuss=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

160
init_data.json.template Normal file
View File

@ -0,0 +1,160 @@
{
"organizations": [
{
"owner": "",
"name": "",
"displayName": "",
"websiteUrl": "",
"favicon": "",
"passwordType": "",
"phonePrefix": "",
"defaultAvatar": "",
"tags": [""]
}
],
"applications": [
{
"owner": "",
"name": "",
"displayName": "",
"logo": "",
"homepageUrl": "",
"organization": "",
"cert": "",
"enablePassword": true,
"enableSignUp": true,
"clientId": "",
"clientSecret": "",
"providers": [
{
"name": "",
"canSignUp": true,
"canSignIn": true,
"canUnlink": false,
"prompted": false,
"alertType": "None"
}
],
"signupItems": [
{
"name": "ID",
"visible": false,
"required": true,
"prompted": false,
"rule": "Random"
},
{
"name": "Username",
"visible": true,
"required": true,
"prompted": false,
"rule": "None"
},
{
"name": "Display name",
"visible": true,
"required": true,
"prompted": false,
"rule": "None"
},
{
"name": "Password",
"visible": true,
"required": true,
"prompted": false,
"rule": "None"
},
{
"name": "Confirm password",
"visible": true,
"required": true,
"prompted": false,
"rule": "None"
},
{
"name": "Email",
"visible": true,
"required": true,
"prompted": false,
"rule": "None"
},
{
"name": "Phone",
"visible": true,
"required": true,
"prompted": false,
"rule": "None"
},
{
"name": "Agreement",
"visible": true,
"required": true,
"prompted": false,
"rule": "None"
}
],
"redirectUris": [""],
"expireInHours": 168
}
],
"users": [
{
"owner": "",
"name": "",
"type": "normal-user",
"password": "",
"displayName": "",
"avatar": "",
"email": "",
"phone": "",
"address": [],
"affiliation": "",
"tag": "",
"score": 2000,
"ranking": 1,
"isAdmin": true,
"isGlobalAdmin": true,
"isForbidden": false,
"isDeleted": false,
"signupApplication": "",
"createdIp": ""
}
],
"providers": [
{
"owner": "",
"name": "",
"displayName": "",
"category": "",
"type": ""
}
],
"certs": [
{
"owner": "",
"name": "",
"displayName": "",
"scope": "JWT",
"type": "x509",
"cryptoAlgorithm": "RS256",
"bitSize": 4096,
"expireInYears": 20,
"publicKey": "",
"privateKey": ""
}
],
"ldaps": [
{
"id": "",
"owner": "",
"serverName": "",
"host": "",
"port": 389,
"admin": "",
"passwd": "",
"baseDn": "",
"autoSync": 0,
"lastSync": ""
}
]
}

View File

@ -36,6 +36,7 @@ func main() {
object.InitAdapter(*createDatabase)
object.InitDb()
object.InitFromFile()
object.InitDefaultStorageProvider()
object.InitLdapAutoSynchronizer()
proxy.InitHttpClient()
@ -51,6 +52,7 @@ func main() {
// https://studygolang.com/articles/2303
beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AuthzFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)

View File

@ -16,12 +16,21 @@ package object
import (
"fmt"
"net/url"
"strings"
"github.com/casdoor/casdoor/util"
"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 {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
@ -37,6 +46,7 @@ type Application struct {
EnableSignUp bool `json:"enableSignUp"`
EnableSigninSession bool `json:"enableSigninSession"`
EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
@ -270,8 +280,12 @@ func UpdateApplication(id string, application *Application) bool {
}
func AddApplication(application *Application) bool {
application.ClientId = util.GenerateClientId()
application.ClientSecret = util.GenerateClientSecret()
if application.ClientId == "" {
application.ClientId = util.GenerateClientId()
}
if application.ClientSecret == "" {
application.ClientSecret = util.GenerateClientSecret()
}
for _, providerItem := range application.Providers {
providerItem.Provider = nil
}
@ -311,3 +325,39 @@ func CheckRedirectUriValid(application *Application, redirectUri string) bool {
}
return validUri
}
func IsAllowOrigin(origin string) bool {
allowOrigin := false
originUrl, err := url.Parse(origin)
if err != nil {
return false
}
rows, err := adapter.Engine.Cols("redirect_uris").Rows(&Application{})
if err != nil {
panic(err)
}
application := Application{}
for rows.Next() {
err := rows.Scan(&application)
if err != nil {
panic(err)
}
for _, tmpRedirectUri := range application.RedirectUris {
u1, err := url.Parse(tmpRedirectUri)
if err != nil {
continue
}
if u1.Scheme == originUrl.Scheme && u1.Host == originUrl.Host {
allowOrigin = true
break
}
}
if allowOrigin {
break
}
}
return allowOrigin
}

View File

@ -51,6 +51,10 @@ func downloadFile(url string) (*bytes.Buffer, error) {
}
func getPermanentAvatarUrl(organization string, username string, url string) string {
if url == "" {
return ""
}
if defaultStorageProvider == nil {
return ""
}

View File

@ -29,3 +29,16 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
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() {
existed := initBuiltInOrganization()
if !existed {
initBuiltInProvider()
initBuiltInUser()
initBuiltInApplication()
initBuiltInCert()
@ -47,6 +48,31 @@ func initBuiltInOrganization() bool {
PhonePrefix: "86",
DefaultAvatar: "https://casbin.org/img/casbin.svg",
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)
return false
@ -78,7 +104,7 @@ func initBuiltInUser() {
IsGlobalAdmin: true,
IsForbidden: false,
IsDeleted: false,
SignupApplication: "built-in-app",
SignupApplication: "app-built-in",
CreatedIp: "127.0.0.1",
Properties: make(map[string]string),
}
@ -102,7 +128,9 @@ func initBuiltInApplication() {
Cert: "cert-built-in",
EnablePassword: 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{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},
{Name: "Username", Visible: true, Required: true, Prompted: false, Rule: "None"},
@ -176,3 +204,20 @@ func initBuiltInLdap() {
}
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)
}

146
object/init_data.go Normal file
View File

@ -0,0 +1,146 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "github.com/casdoor/casdoor/util"
type InitData struct {
Organizations []*Organization `json:"organizations"`
Applications []*Application `json:"applications"`
Users []*User `json:"users"`
Certs []*Cert `json:"certs"`
Providers []*Provider `json:"providers"`
Ldaps []*Ldap `json:"ldaps"`
}
func InitFromFile() {
initData := readInitDataFromFile("./init_data.json")
if initData != nil {
for _, organization := range initData.Organizations {
initDefinedOrganization(organization)
}
for _, provider := range initData.Providers {
initDefinedProvider(provider)
}
for _, user := range initData.Users {
initDefinedUser(user)
}
for _, application := range initData.Applications {
initDefinedApplication(application)
}
for _, cert := range initData.Certs {
initDefinedCert(cert)
}
for _, ldap := range initData.Ldaps {
initDefinedLdap(ldap)
}
}
}
func readInitDataFromFile(filePath string) *InitData {
if !util.FileExist(filePath) {
return nil
}
s := util.ReadStringFromPath(filePath)
data := &InitData{}
err := util.JsonToStruct(s, data)
if err != nil {
panic(err)
}
return data
}
func initDefinedOrganization(organization *Organization) {
existed := getOrganization(organization.Owner, organization.Name)
if existed != nil {
return
}
organization.CreatedTime = util.GetCurrentTime()
organization.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)
}
func initDefinedApplication(application *Application) {
existed := getApplication(application.Owner, application.Name)
if existed != nil {
return
}
application.CreatedTime = util.GetCurrentTime()
AddApplication(application)
}
func initDefinedUser(user *User) {
existed := getUser(user.Owner, user.Name)
if existed != nil {
return
}
user.CreatedTime = util.GetCurrentTime()
user.Id = util.GenerateId()
user.Properties = make(map[string]string)
AddUser(user)
}
func initDefinedCert(cert *Cert) {
existed := getCert(cert.Owner, cert.Name)
if existed != nil {
return
}
cert.CreatedTime = util.GetCurrentTime()
AddCert(cert)
}
func initDefinedLdap(ldap *Ldap) {
existed := GetLdap(ldap.Id)
if existed != nil {
return
}
AddLdap(ldap)
}
func initDefinedProvider(provider *Provider) {
existed := GetProvider(provider.GetId())
if existed != nil {
return
}
AddProvider(provider)
}

View File

@ -20,6 +20,13 @@ import (
"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 {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
@ -36,6 +43,8 @@ type Organization struct {
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
AccountItems []*AccountItem `xorm:"varchar(2000)" json:"accountItems"`
}
func GetOrganizationCount(owner, field, value string) int {

View File

@ -32,10 +32,10 @@ func TestProduct(t *testing.T) {
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)
paymentId := util.GenerateTimeId()
paymentName := util.GenerateTimeId()
returnUrl := ""
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 {
panic(err)
}

View File

@ -142,8 +142,8 @@ func GetProvider(id string) *Provider {
return getProvider(owner, name)
}
func GetDefaultHumanCheckProvider() *Provider {
provider := Provider{Owner: "admin", Category: "HumanCheck"}
func GetDefaultCaptchaProvider() *Provider {
provider := Provider{Owner: "admin", Category: "Captcha"}
existed, err := adapter.Engine.Get(&provider)
if err != nil {
panic(err)
@ -225,3 +225,37 @@ func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
func (p *Provider) GetId() string {
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

@ -36,7 +36,7 @@ import (
)
//returns a saml2 response
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, redirectUri []string) (*etree.Element, error) {
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
samlResponse := &etree.Element{
Space: "samlp",
Tag: "Response",
@ -51,7 +51,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
samlResponse.CreateAttr("Version", "2.0")
samlResponse.CreateAttr("IssueInstant", now)
samlResponse.CreateAttr("Destination", destination)
samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("Casdoor_%s", arId))
samlResponse.CreateAttr("InResponseTo", requestId)
samlResponse.CreateElement("saml:Issuer").SetText(host)
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
@ -68,7 +68,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer")
subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData")
subjectConfirmationData.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId))
subjectConfirmationData.CreateAttr("InResponseTo", requestId)
subjectConfirmationData.CreateAttr("Recipient", destination)
subjectConfirmationData.CreateAttr("NotOnOrAfter", expireTime)
condition := assertion.CreateElement("saml:Conditions")
@ -225,14 +225,15 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
return &d, nil
}
//GenerateSamlResponse generates a SAML2.0 response
//parameter samlRequest is saml request in base64 format
// GetSamlResponse generates a SAML2.0 response
// parameter samlRequest is saml request in base64 format
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, error) {
//decode samlRequest
// base64 decode
defated, err := base64.StdEncoding.DecodeString(samlRequest)
if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error())
}
// decompress
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
io.Copy(&buffer, rdr)
@ -241,42 +242,58 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error())
}
//verify samlRequest
// verify samlRequest
if valid := CheckRedirectUriValid(application, authnRequest.Issuer.Url); !valid {
return "", "", fmt.Errorf("err: invalid issuer url")
}
//get publickey string
// get public key string
cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.PublicKey))
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
_, originBackend := getOriginFromHost(host)
//build signedResponse
samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, application.RedirectUris)
// build signedResponse
samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey,
X509Certificate: publicKey,
}
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1
signedXML, err := ctx.SignEnveloped(samlResponse)
//signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
//if err != nil {
// return "", "", fmt.Errorf("err: %s", err.Error())
//}
sig, err := ctx.ConstructSignature(samlResponse, true)
samlResponse.InsertChildAt(1, sig)
doc := etree.NewDocument()
doc.SetRoot(samlResponse)
xmlBytes, err := doc.WriteToBytes()
if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error())
}
doc := etree.NewDocument()
doc.SetRoot(signedXML)
xmlStr, err := doc.WriteToString()
if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error())
// compress
if application.EnableSamlCompress {
flated := bytes.NewBuffer(nil)
writer, err := flate.NewWriter(flated, flate.DefaultCompression)
if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error())
}
writer.Write(xmlBytes)
writer.Close()
xmlBytes = flated.Bytes()
}
res := base64.StdEncoding.EncodeToString([]byte(xmlStr))
// base64 encode
res := base64.StdEncoding.EncodeToString(xmlBytes)
return res, authnRequest.AssertionConsumerServiceURL, nil
}
//return a saml1.1 response(not 2.0)
// NewSamlResponse11 return a saml1.1 response(not 2.0)
func NewSamlResponse11(user *User, requestID string, host string) *etree.Element {
samlResponse := &etree.Element{
Space: "samlp",

View File

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

View File

@ -17,7 +17,6 @@ package object
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
@ -28,7 +27,14 @@ import (
)
const (
hourSeconds = 3600
hourSeconds = 3600
INVALID_REQUEST = "invalid_request"
INVALID_CLIENT = "invalid_client"
INVALID_GRANT = "invalid_grant"
UNAUTHORIZED_CLIENT = "unauthorized_client"
UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"
INVALID_SCOPE = "invalid_scope"
ENDPOINT_ERROR = "endpoint_error"
)
type Code struct {
@ -63,7 +69,11 @@ type TokenWrapper struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
Error string `json:"error,omitempty"`
}
type TokenError struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description,omitempty"`
}
type IntrospectionResponse struct {
@ -311,59 +321,42 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, tag string, avatar string) *TokenWrapper {
var errString string
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, tag string, avatar string) interface{} {
application := GetApplicationByClientId(clientId)
if application == nil {
errString = "error: invalid client_id"
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: INVALID_CLIENT,
ErrorDescription: "client_id is invalid",
}
}
//Check if grantType is allowed in the current application
if !IsGrantTypeValid(grantType, application.GrantTypes) && tag == "" {
errString = fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType)
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: UNSUPPORTED_GRANT_TYPE,
ErrorDescription: fmt.Sprintf("grant_type: %s is not supported in this application", grantType),
}
}
var token *Token
var err error
var tokenError *TokenError
switch grantType {
case "authorization_code": // Authorization Code Grant
token, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
token, tokenError = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
case "password": // Resource Owner Password Credentials Grant
token, err = GetPasswordToken(application, username, password, scope, host)
token, tokenError = GetPasswordToken(application, username, password, scope, host)
case "client_credentials": // Client Credentials Grant
token, err = GetClientCredentialsToken(application, clientSecret, scope, host)
token, tokenError = GetClientCredentialsToken(application, clientSecret, scope, host)
}
if tag == "wechat_miniprogram" {
// Wechat Mini Program
token, err = GetWechatMiniProgramToken(application, code, host, username, avatar)
token, tokenError = GetWechatMiniProgramToken(application, code, host, username, avatar)
}
if err != nil {
errString = err.Error()
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
}
if tokenError != nil {
return tokenError
}
token.CodeIsUsed = true
@ -380,81 +373,59 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
return tokenWrapper
}
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) *TokenWrapper {
var errString string
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) interface{} {
// check parameters
if grantType != "refresh_token" {
errString = "error: grant_type should be \"refresh_token\""
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: UNSUPPORTED_GRANT_TYPE,
ErrorDescription: "grant_type should be refresh_token",
}
}
application := GetApplicationByClientId(clientId)
if application == nil {
errString = "error: invalid client_id"
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: INVALID_CLIENT,
ErrorDescription: "client_id is invalid",
}
}
if clientSecret != "" && application.ClientSecret != clientSecret {
errString = "error: invalid client_secret"
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: INVALID_CLIENT,
ErrorDescription: "client_secret is invalid",
}
}
// check whether the refresh token is valid, and has not expired.
token := Token{RefreshToken: refreshToken}
existed, err := adapter.Engine.Get(&token)
if err != nil || !existed {
errString = "error: invalid refresh_token"
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "refresh token is invalid, expired or revoked",
}
}
cert := getCertByApplication(application)
_, err = ParseJwtToken(refreshToken, cert)
if err != nil {
errString := fmt.Sprintf("error: %s", err.Error())
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: INVALID_GRANT,
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}
}
// generate a new token
user := getUser(application.Organization, token.User)
if user.IsForbidden {
errString = "error: the user is forbidden to sign in, please contact the administrator"
return &TokenWrapper{
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
return &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
}
}
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
panic(err)
return &TokenError{
Error: ENDPOINT_ERROR,
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
}
}
newToken := &Token{
@ -508,63 +479,99 @@ func IsGrantTypeValid(method string, grantTypes []string) bool {
}
// Authorization code flow
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, error) {
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, *TokenError) {
if code == "" {
return nil, errors.New("error: authorization code should not be empty")
return nil, &TokenError{
Error: INVALID_REQUEST,
ErrorDescription: "authorization code should not be empty",
}
}
token := getTokenByCode(code)
if token == nil {
return nil, errors.New("error: invalid authorization code")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "authorization code is invalid",
}
}
if token.CodeIsUsed {
// anti replay attacks
return nil, errors.New("error: authorization code has been used")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "authorization code has been used",
}
}
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
return nil, errors.New("error: incorrect code_verifier")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "verifier is invalid",
}
}
if application.ClientSecret != clientSecret {
// when using PKCE, the Client Secret can be empty,
// but if it is provided, it must be accurate.
if token.CodeChallenge == "" {
return nil, errors.New("error: invalid client_secret")
return nil, &TokenError{
Error: INVALID_CLIENT,
ErrorDescription: "client_secret is invalid",
}
} else {
if clientSecret != "" {
return nil, errors.New("error: invalid client_secret")
return nil, &TokenError{
Error: INVALID_CLIENT,
ErrorDescription: "client_secret is invalid",
}
}
}
}
if application.Name != token.Application {
return nil, errors.New("error: the token is for wrong application (client_id)")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "the token is for wrong application (client_id)",
}
}
if time.Now().Unix() > token.CodeExpireIn {
// code must be used within 5 minutes
return nil, errors.New("error: authorization code has expired")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "authorization code has expired",
}
}
return token, nil
}
// Resource Owner Password Credentials flow
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, error) {
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError) {
user := getUser(application.Organization, username)
if user == nil {
return nil, errors.New("error: the user does not exist")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "the user does not exist",
}
}
msg := CheckPassword(user, password)
if msg != "" {
return nil, errors.New("error: invalid username or password")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "invalid username or password",
}
}
if user.IsForbidden {
return nil, errors.New("error: the user is forbidden to sign in, please contact the administrator")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
}
}
accessToken, refreshToken, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, err
return nil, &TokenError{
Error: ENDPOINT_ERROR,
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
}
}
token := &Token{
Owner: application.Owner,
@ -586,9 +593,12 @@ func GetPasswordToken(application *Application, username string, password string
}
// Client Credentials flow
func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, error) {
func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, *TokenError) {
if application.ClientSecret != clientSecret {
return nil, errors.New("error: invalid client_secret")
return nil, &TokenError{
Error: INVALID_CLIENT,
ErrorDescription: "client_secret is invalid",
}
}
nullUser := &User{
Owner: application.Owner,
@ -597,7 +607,10 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
}
accessToken, _, err := generateJwtToken(application, nullUser, "", scope, host)
if err != nil {
return nil, err
return nil, &TokenError{
Error: ENDPOINT_ERROR,
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
}
}
token := &Token{
Owner: application.Owner,
@ -643,25 +656,37 @@ func GetTokenByUser(application *Application, user *User, scope string, host str
}
// Wechat Mini Program flow
func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string) (*Token, error) {
func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string) (*Token, *TokenError) {
mpProvider := GetWechatMiniProgramProvider(application)
if mpProvider == nil {
return nil, errors.New("error: the application does not support wechat mini program")
return nil, &TokenError{
Error: INVALID_CLIENT,
ErrorDescription: "the application does not support wechat mini program",
}
}
provider := GetProvider(util.GetId(mpProvider.Name))
mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret)
session, err := mpIdp.GetSessionByCode(code)
if err != nil {
return nil, err
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: fmt.Sprintf("get wechat mini program session error: %s", err.Error()),
}
}
openId, unionId := session.Openid, session.Unionid
if openId == "" && unionId == "" {
return nil, errors.New("err: WeChat's openid and unionid are empty")
return nil, &TokenError{
Error: INVALID_REQUEST,
ErrorDescription: "the wechat mini program session is invalid",
}
}
user := getUserByWechatId(openId, unionId)
if user == nil {
if !application.EnableSignUp {
return nil, errors.New("err: the application does not allow to sign up new account")
return nil, &TokenError{
Error: INVALID_GRANT,
ErrorDescription: "the application does not allow to sign up new account",
}
}
//Add new user
var name string
@ -691,7 +716,10 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
accessToken, refreshToken, err := generateJwtToken(application, user, "", "", host)
if err != nil {
return nil, err
return nil, &TokenError{
Error: ENDPOINT_ERROR,
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
}
}
token := &Token{

View File

@ -76,7 +76,7 @@ func getProxyHttpClient() *http.Client {
}
func GetHttpClient(url string) *http.Client {
if strings.Contains(url, "githubusercontent.com") {
if strings.Contains(url, "githubusercontent.com") || strings.Contains(url, "googleusercontent.com") {
return ProxyHttpClient
} else {
return DefaultHttpClient

51
routers/cors_filter.go Normal file
View File

@ -0,0 +1,51 @@
// 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 routers
import (
"net/http"
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
)
const (
headerOrigin = "Origin"
headerAllowOrigin = "Access-Control-Allow-Origin"
headerAllowMethods = "Access-Control-Allow-Methods"
headerAllowHeaders = "Access-Control-Allow-Headers"
)
func CorsFilter(ctx *context.Context) {
origin := ctx.Input.Header(headerOrigin)
originConf := conf.GetConfigString("origin")
if origin != "" && originConf != "" && origin != originConf {
if object.IsAllowOrigin(origin) {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
} else {
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
return
}
if ctx.Input.Method() == "OPTIONS" {
ctx.ResponseWriter.WriteHeader(http.StatusOK)
return
}
}
}

View File

@ -94,8 +94,9 @@ func initAPI() {
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone")
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/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-ldaps", &controllers.ApiController{}, "POST:GetLdaps")

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,22 @@ paths:
tags:
- OIDC API
operationId: RootController.GetJwks
responses:
"200":
description: ""
schema:
$ref: '#/definitions/jose.JSONWebKey'
/.well-known/openid-configuration:
get:
tags:
- OIDC API
description: Get Oidc Discovery
operationId: RootController.GetOidcDiscovery
responses:
"200":
description: ""
schema:
$ref: '#/definitions/object.OidcDiscovery'
/api/add-application:
post:
tags:
@ -58,6 +69,24 @@ paths:
tags:
- Account API
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:
post:
tags:
@ -243,11 +272,11 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/get-human-check:
/api/api/get-captcha:
get:
tags:
- Login API
operationId: ApiController.GetHumancheck
operationId: ApiController.GetCaptcha
/api/api/reset-email-or-phone:
post:
tags:
@ -271,11 +300,11 @@ paths:
required: true
type: string
- in: body
name: body
name: from
description: Details of the email request
required: true
schema:
$ref: '#/definitions/emailForm'
$ref: '#/definitions/controllers.EmailForm'
responses:
"200":
description: object
@ -299,11 +328,11 @@ paths:
required: true
type: string
- in: body
name: body
name: from
description: Details of the sms request
required: true
schema:
$ref: '#/definitions/smsForm'
$ref: '#/definitions/controllers.SmsForm'
responses:
"200":
description: object
@ -382,6 +411,24 @@ paths:
tags:
- Account API
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:
post:
tags:
@ -578,6 +625,43 @@ paths:
description: The Response object
schema:
$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:
get:
tags:
@ -700,6 +784,42 @@ paths:
tags:
- Account API
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:
get:
tags:
@ -901,9 +1021,7 @@ paths:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Records'
$ref: '#/definitions/object.Record'
/api/get-records-filter:
post:
tags:
@ -912,18 +1030,17 @@ paths:
operationId: ApiController.GetRecordsByFilter
parameters:
- in: body
name: body
name: filter
description: filter Record message
required: true
schema:
$ref: '#/definitions/object.Records'
type: string
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Records'
$ref: '#/definitions/object.Record'
/api/get-resource:
get:
tags:
@ -1216,6 +1333,23 @@ paths:
type: array
items:
$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:
post:
tags:
@ -1224,21 +1358,51 @@ paths:
operationId: ApiController.Login
parameters:
- in: query
name: oAuthParams
description: oAuth parameters
name: clientId
description: clientId
required: true
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
name: body
name: form
description: Login information
required: true
schema:
$ref: '#/definitions/RequestForm'
$ref: '#/definitions/controllers.RequestForm'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.api_controller.Response'
$ref: '#/definitions/Response'
/api/login/oauth/access_token:
post:
tags:
@ -1271,6 +1435,14 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/object.TokenWrapper'
"400":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
"401":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
/api/login/oauth/code:
post:
tags:
@ -1333,6 +1505,14 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/object.IntrospectionResponse'
"400":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
"401":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
/api/login/oauth/logout:
get:
tags:
@ -1395,6 +1575,14 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/object.TokenWrapper'
"400":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
"401":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
/api/logout:
post:
tags:
@ -1424,6 +1612,24 @@ paths:
description: The Response object
schema:
$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:
post:
tags:
@ -1493,42 +1699,6 @@ paths:
tags:
- Login API
/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:
tags:
- Application API
@ -1579,6 +1749,29 @@ paths:
tags:
- Account API
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:
post:
tags:
@ -1830,27 +2023,99 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/verify-captcha:
post:
tags:
- Verification API
operationId: ApiController.VerifyCaptcha
definitions:
2127.0xc00036c600.false:
2127.0xc000398090.false:
title: "false"
type: object
2161.0xc00036c630.false:
2161.0xc0003980c0.false:
title: "false"
type: object
RequestForm:
title: RequestForm
type: object
Response:
title: Response
type: object
controllers.EmailForm:
title: EmailForm
type: object
properties:
content:
type: string
provider:
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:
title: Response
type: object
properties:
data:
$ref: '#/definitions/2127.0xc00036c600.false'
$ref: '#/definitions/2127.0xc000398090.false'
data2:
$ref: '#/definitions/2161.0xc00036c630.false'
$ref: '#/definitions/2161.0xc0003980c0.false'
msg:
type: string
name:
@ -1859,25 +2124,33 @@ definitions:
type: string
sub:
type: string
controllers.api_controller.Response:
title: Response
controllers.SmsForm:
title: SmsForm
type: object
properties:
data:
$ref: '#/definitions/2127.0xc00036c600.false'
data2:
$ref: '#/definitions/2161.0xc00036c630.false'
msg:
content:
type: string
organizationId:
type: string
receivers:
type: array
items:
type: string
jose.JSONWebKey:
title: JSONWebKey
type: object
object.AccountItem:
title: AccountItem
type: object
properties:
modifyRule:
type: string
name:
type: string
status:
viewRule:
type: string
sub:
type: string
emailForm:
title: emailForm
type: object
visible:
type: boolean
object.Adapter:
title: Adapter
type: object
@ -2037,10 +2310,80 @@ definitions:
type: string
username:
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:
title: Organization
type: object
properties:
accountItems:
type: array
items:
$ref: '#/definitions/object.AccountItem'
createdTime:
type: string
defaultAvatar:
@ -2051,6 +2394,8 @@ definitions:
type: boolean
favicon:
type: string
isProfilePublic:
type: boolean
masterPassword:
type: string
name:
@ -2081,6 +2426,16 @@ definitions:
type: string
displayName:
type: string
invoiceRemark:
type: string
invoiceTaxId:
type: string
invoiceTitle:
type: string
invoiceType:
type: string
invoiceUrl:
type: string
message:
type: string
name:
@ -2091,6 +2446,14 @@ definitions:
type: string
payUrl:
type: string
personEmail:
type: string
personIdCard:
type: string
personName:
type: string
personPhone:
type: string
price:
type: number
format: double
@ -2126,6 +2489,8 @@ definitions:
type: string
isEnabled:
type: boolean
model:
type: string
name:
type: string
owner:
@ -2205,6 +2570,16 @@ definitions:
type: string
createdTime:
type: string
customAuthUrl:
type: string
customLogo:
type: string
customScope:
type: string
customTokenUrl:
type: string
customUserInfoUrl:
type: string
displayName:
type: string
domain:
@ -2264,9 +2639,35 @@ definitions:
type: boolean
provider:
$ref: '#/definitions/object.Provider'
object.Records:
title: Records
object.Record:
title: Record
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:
title: Role
type: object
@ -2401,14 +2802,20 @@ definitions:
type: string
user:
type: string
object.TokenError:
title: TokenError
type: object
properties:
error:
type: string
error_description:
type: string
object.TokenWrapper:
title: TokenWrapper
type: object
properties:
access_token:
type: string
error:
type: string
expires_in:
type: integer
format: int64
@ -2442,6 +2849,8 @@ definitions:
type: string
baidu:
type: string
bilibili:
type: string
bio:
type: string
birthday:
@ -2452,10 +2861,14 @@ definitions:
type: string
createdTime:
type: string
custom:
type: string
dingtalk:
type: string
displayName:
type: string
douyin:
type: string
education:
type: string
email:
@ -2521,6 +2934,8 @@ definitions:
type: string
name:
type: string
okta:
type: string
owner:
type: string
password:
@ -2558,6 +2973,8 @@ definitions:
type: string
type:
type: string
unionId:
type: string
updatedTime:
type: string
wechat:
@ -2618,9 +3035,6 @@ definitions:
type: string
url:
type: string
smsForm:
title: smsForm
type: object
xorm.Engine:
title: Engine
type: object

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");
// you may not use this file except in compliance with the License.
@ -12,12 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package object
package util
type SignupItem struct {
Name string `json:"name"`
Visible bool `json:"visible"`
Required bool `json:"required"`
Prompted bool `json:"prompted"`
Rule string `json:"rule"`
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
)
func GetHmacSha1(keyStr, value string) string {
key := []byte(keyStr)
mac := hmac.New(sha1.New, key)
mac.Write([]byte(value))
res := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return res
}

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

@ -663,24 +663,29 @@ class App extends Component {
renderPage() {
if (this.isDoorPages()) {
return (
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)}/>
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />)}/>
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)}/>
<Route exact path="/signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage type={"cas"} mode={"signup"} account={this.state.account} {...props} />)}} />
<Route exact path="/callback" component={AuthCallback}/>
<Route exact path="/callback/saml" component={SamlCallback}/>
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)}/>
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...props} />)}/>
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} {...props} />)}/>
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} {...props} />)}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}/>} />
</Switch>
<div>
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)}/>
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />)}/>
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)}/>
<Route exact path="/signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage type={"cas"} mode={"signup"} account={this.state.account} {...props} />)}} />
<Route exact path="/callback" component={AuthCallback}/>
<Route exact path="/callback/saml" component={SamlCallback}/>
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)}/>
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...props} />)}/>
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} {...props} />)}/>
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} {...props} />)}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}/>} />
</Switch>
{
this.renderFooter()
}
</div>
)
}

View File

@ -14,7 +14,7 @@
import React from "react";
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 CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting";
@ -28,14 +28,15 @@ import UrlTable from "./UrlTable";
import ProviderTable from "./ProviderTable";
import SignupTable from "./SignupTable";
import PromptPage from "./auth/PromptPage";
import copy from "copy-to-clipboard";
import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css";
require('codemirror/theme/material-darker.css');
require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml");
const { Option } = Select;
const { TextArea } = Input;
class ApplicationEditPage extends React.Component {
constructor(props) {
@ -180,7 +181,7 @@ class ApplicationEditPage extends React.Component {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: '100%'} :{}}>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
@ -402,7 +403,7 @@ class ApplicationEditPage extends React.Component {
}}/>
<Upload maxCount={1} accept=".html" showUploadList={false}
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>
</Col>
</Row>
@ -473,12 +474,34 @@ class ApplicationEditPage extends React.Component {
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable SAML compress"), i18next.t("application:Enable SAML compress - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.application.enableSamlCompress} onChange={checked => {
this.updateApplicationField('enableSamlCompress', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={8} value={this.state.samlMetadata} />
<CodeMirror
value={this.state.samlMetadata}
options={{mode: 'xml', theme: 'default'}}
onBeforeChange={(editor, data, value) => {}}
/>
<br/>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}`);
Setting.showMessage("success", i18next.t("application:SAML metadata URL copied to clipboard successfully"));
}}
>
{i18next.t("application:Copy SAML metadata URL")}
</Button>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
@ -500,7 +523,7 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
{
this.renderPreview()
this.renderSignupSigninPreview()
}
</Row>
{
@ -524,30 +547,33 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
{
this.renderPreview2()
this.renderPromptPreview()
}
</Row>
</Card>
)
}
renderPreview() {
renderSignupSigninPreview() {
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 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) {
signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize");
}
if (!Setting.isMobile()) {
return (
<React.Fragment>
<Col span={11} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Button type="primary">{i18next.t("application:Test signup page..")}</Button>
</a>
<Col span={11}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signUpUrl}`);
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/>
<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={{position: "relative", 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} />
@ -558,40 +584,16 @@ class ApplicationEditPage extends React.Component {
<div style={maskStyle}></div>
</div>
</Col>
<Col span={11} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}>
<Button type="primary">{i18next.t("application:Test signin page..")}</Button>
</a>
<Col span={11}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signInUrl}`);
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/>
<div style={{position:'relative', width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle}></div>
</div>
</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={{position:'relative', 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 style={maskStyle}></div>
</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={{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={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle}></div>
</div>
@ -599,24 +601,25 @@ class ApplicationEditPage extends React.Component {
</React.Fragment>
)
}
}
renderPreview2() {
renderPromptPreview() {
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 (
<React.Fragment>
<Col span={(Setting.isMobile()) ? 24 : 11} style={{display:"flex", flexDirection: "column", flex: "auto"}} >
<a style={{marginBottom: "10px"}} target="_blank" rel="noreferrer" href={promptUrl}>
<Button type="primary">{i18next.t("application:Test prompt page..")}</Button>
</a>
<br style={(Setting.isMobile()) ? {display: "none"} : {}} />
<br style={(Setting.isMobile()) ? {display: "none"} : {}} />
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
</div>
</Col>
</React.Fragment>
<Col span={11}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${promptUrl}`);
Setting.showMessage("success", i18next.t("application:Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
>
{i18next.t("application:Copy prompt page URL")}
</Button>
<br/>
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
<div style={maskStyle}></div>
</div>
</Col>
)
}

View File

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

View File

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

View File

@ -132,7 +132,7 @@ class ModelEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("model:Model"), i18next.t("model:Model - Tooltip"))} :
{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 => {

View File

@ -20,6 +20,7 @@ import * as Setting from "./Setting";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./LdapTable";
import AccountTable from "./AccountTable";
const { Option } = Select;
@ -117,7 +118,7 @@ class OrganizationEditPage extends React.Component {
</Col>
<Col span={22} >
<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"))} :
</Col>
<Col span={23} >
@ -127,7 +128,7 @@ class OrganizationEditPage extends React.Component {
</Col>
</Row>
<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")}:
</Col>
<Col span={23} >
@ -187,7 +188,7 @@ class OrganizationEditPage extends React.Component {
</Col>
<Col span={22} >
<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"))} :
</Col>
<Col span={23} >
@ -197,7 +198,7 @@ class OrganizationEditPage extends React.Component {
</Col>
</Row>
<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")}:
</Col>
<Col span={23} >
@ -250,6 +251,18 @@ class OrganizationEditPage extends React.Component {
}} />
</Col>
</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'}}>
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{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";
class OrganizationListPage extends BaseListPage {
newOrganization() {
const randomName = Setting.getRandomName();
return {
@ -40,6 +39,31 @@ class OrganizationListPage extends BaseListPage {
masterPassword: "",
enableSoftDeletion: false,
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

@ -93,7 +93,7 @@ class PermissionListPage extends BaseListPage {
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/permissions/${text}`}>
<Link to={`/permissions/${record.owner}/${text}`}>
{text}
</Link>
)

View File

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

View File

@ -19,7 +19,9 @@ import * as ProviderBackend from "./backend/ProviderBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import { authConfig } from "./auth/Auth";
import * as ProviderEditTestEmail from "./TestEmailWidget";
import copy from 'copy-to-clipboard';
import { CaptchaPreview } from "./common/CaptchaPreview";
const { Option } = Select;
const { TextArea } = Input;
@ -32,6 +34,7 @@ class ProviderEditPage extends React.Component {
providerName: props.match.params.providerName,
provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
testEmail: this.props.account["email"] !== undefined ? this.props.account["email"] : "",
};
}
@ -77,6 +80,12 @@ class ProviderEditPage extends React.Component {
} else {
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
case "Captcha":
if (this.state.provider.type === "Aliyun Captcha") {
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Site key"), i18next.t("provider:Site key - Tooltip"));
}
default:
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
@ -94,6 +103,12 @@ class ProviderEditPage extends React.Component {
} else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
case "Captcha":
if (this.state.provider.type === "Aliyun Captcha") {
return Setting.getLabel(i18next.t("provider:Secret access key"), i18next.t("provider:SecretAccessKey - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
}
default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
@ -193,6 +208,8 @@ class ProviderEditPage extends React.Component {
this.updateProviderField('domain', Setting.getFullServerUrl());
} else if (value === "SAML") {
this.updateProviderField('type', 'Aliyun IDaaS');
} else if (value === "Captcha") {
this.updateProviderField('type', 'Default');
}
})}>
{
@ -203,6 +220,7 @@ class ProviderEditPage extends React.Component {
{id: 'Storage', name: 'Storage'},
{id: 'SAML', name: 'SAML'},
{id: 'Payment', name: 'Payment'},
{id: 'Captcha', name: 'Captcha'},
].map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
}
</Select>
@ -232,7 +250,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
{
this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" ? null : (
this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" && this.state.provider.type !== "Aliyun Captcha" ? null : (
<React.Fragment>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
@ -249,7 +267,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
{
this.state.provider.type !== "WeCom" ? null : (
this.state.provider.type !== "WeCom" ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Method"), i18next.t("provider:Method - Tooltip"))} :
@ -317,7 +335,7 @@ class ProviderEditPage extends React.Component {
</Col>
<Col span={22} >
<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"))} :
</Col>
<Col span={23} >
@ -327,7 +345,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
<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")}:
</Col>
<Col span={23} >
@ -341,32 +359,40 @@ class ProviderEditPage extends React.Component {
</React.Fragment>
)
}
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel()}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField('clientId', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecretLabel()}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret} onChange={e => {
this.updateProviderField('clientSecret', e.target.value);
}} />
</Col>
</Row>
{
this.state.provider.type !== "WeChat" ? null : (
this.state.provider.category === "Captcha" && this.state.provider.type === "Default" ? null : (
<React.Fragment>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))}
{this.getClientIdLabel()}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField('clientId', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecretLabel()}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret} onChange={e => {
this.updateProviderField('clientSecret', e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
)
}
{
this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" ? null : (
<React.Fragment>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{this.state.provider.type === "Aliyun Captcha"
? Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"))
: Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId2} onChange={e => {
@ -376,7 +402,9 @@ class ProviderEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"))}
{this.state.provider.type === "Aliyun Captcha"
? Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"))
: Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret2} onChange={e => {
@ -500,6 +528,27 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</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>
) : this.state.provider.category === "SMS" ? (
<React.Fragment>
@ -637,6 +686,30 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</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}
subType={this.state.provider.subType}
owner={this.state.provider.owner}
clientId={this.state.provider.clientId}
name={this.state.provider.name}
providerUrl={this.state.provider.providerUrl}
clientId2={this.state.provider.clientId2}
clientSecret2={this.state.provider.clientSecret2}
/>
</Col>
</Row>
)
}
</Card>
)
}

View File

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

View File

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

View File

@ -106,6 +106,24 @@ export const OtherProviderInfo = {
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",
},
"Aliyun Captcha": {
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
url: "https://help.aliyun.com/product/28308.html",
}
}
};
export function getCountryRegionData() {
@ -595,6 +613,13 @@ export function getProviderTypeOptions(category) {
{id: 'PayPal', name: 'PayPal'},
{id: 'GC', name: 'GC'},
]);
} else if (category === "Captcha") {
return ([
{id: 'Default', name: 'Default'},
{id: 'reCAPTCHA', name: 'reCAPTCHA'},
{id: 'hCaptcha', name: 'hCaptcha'},
{id: 'Aliyun Captcha', name: 'Aliyun Captcha'},
]);
} else {
return [];
}
@ -608,6 +633,11 @@ export function getProviderSubTypeOptions(type) {
{id: 'Third-party', name: 'Third-party'},
]
);
} else if (type === "Aliyun Captcha") {
return [
{id: 'nc', name: 'Sliding Validation'},
{id: 'ic', name: 'Intelligent Validation'},
];
} else {
return [];
}

View File

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

View File

@ -22,7 +22,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class SyncerListPage extends BaseListPage {
newSyncer() {
const randomName = Setting.getRandomName();
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";
class TokenListPage extends BaseListPage {
newToken() {
const randomName = Setting.getRandomName();
return {

View File

@ -28,10 +28,10 @@ import OAuthWidget from "./common/OAuthWidget";
import SamlWidget from "./common/SamlWidget";
import SelectRegionBox from "./SelectRegionBox";
// import {Controlled as CodeMirror} from 'react-codemirror2';
// import "codemirror/lib/codemirror.css";
// require('codemirror/theme/material-darker.css');
// require("codemirror/mode/javascript/javascript");
import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css";
require('codemirror/theme/material-darker.css');
require("codemirror/mode/javascript/javascript");
const { Option } = Select;
@ -124,46 +124,86 @@ class UserEditPage extends React.Component {
return (this.state.user.id === this.props.account?.id) || Setting.isAdminUser(this.props.account);
}
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">
renderAccountItem(accountItem) {
if (!accountItem.visible) {
return null;
}
const isSelf = this.state.user.id === this.props.account?.id;
const isAdmin = Setting.isAdminUser(this.props.account);
// return (
// <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'}} >
<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%'}} 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>)
}
</Select>
</Col>
</Row>
)
} else if (accountItem.name === "ID") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("ID", i18next.t("general:ID - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.id} disabled={true} />
<Input value={this.state.user.id} disabled={disabled} />
</Col>
</Row>
)
} else if (accountItem.name === "Name") {
return (
<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.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);
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Display name") {
return (
<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"))} :
@ -174,6 +214,9 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Avatar") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} :
@ -204,6 +247,9 @@ class UserEditPage extends React.Component {
</Row>
</Col>
</Row>
)
} else if (accountItem.name === "User type") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User type"), i18next.t("general:User type - Tooltip"))} :
@ -217,44 +263,56 @@ class UserEditPage extends React.Component {
</Select>
</Col>
</Row>
)
} else if (accountItem.name === "Password") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col>
<Col span={22} >
<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>
</Row>
)
} else if (accountItem.name === "Email") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
</Col>
<Col style={{paddingRight: '20px'}} span={11} >
<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 => {
this.updateUserField('email', e.target.value);
}} />
this.updateUserField('email', e.target.value);
}} />
</Col>
<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}
</Col>
</Row>
)
} else if (accountItem.name === "Phone") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
</Col>
<Col style={{paddingRight: '20px'}} span={11} >
<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 => {
this.updateUserField('phone', e.target.value);
this.updateUserField('phone', e.target.value);
}}/>
</Col>
<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}
</Col>
</Row>
)
} else if (accountItem.name === "Country/Region") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Country/Region"), i18next.t("user:Country/Region - Tooltip"))} :
@ -265,6 +323,9 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Location") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Location"), i18next.t("user:Location - Tooltip"))} :
@ -275,11 +336,15 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
{
(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 === "Affiliation") {
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'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
@ -290,6 +355,9 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Homepage") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Homepage"), i18next.t("user:Homepage - Tooltip"))} :
@ -300,6 +368,9 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Bio") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Bio"), i18next.t("user:Bio - Tooltip"))} :
@ -310,6 +381,9 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Tag") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
@ -335,102 +409,138 @@ class UserEditPage extends React.Component {
}
</Col>
</Row>
)
} else if (accountItem.name === "Signup application") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Signup application"), i18next.t("general:Signup application - Tooltip"))} :
</Col>
<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>)
}
</Select>
</Col>
</Row>
{
!this.isSelfOrAdmin() ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:3rd-party logins"), i18next.t("user:3rd-party logins - Tooltip"))} :
</Col>
<Col span={22} >
<div style={{marginBottom: 20}}>
{
(this.state.application === null || this.state.user === null) ? null : (
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) =>
(providerItem.provider.category === "OAuth") ? (
<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()}} />
)
)
} else if (accountItem.name === "3rd-party logins") {
return (
!this.isSelfOrAdmin() ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:3rd-party logins"), i18next.t("user:3rd-party logins - Tooltip"))} :
</Col>
<Col span={22} >
<div style={{marginBottom: 20}}>
{
(this.state.application === null || this.state.user === null) ? null : (
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) =>
(providerItem.provider.category === "OAuth") ? (
<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>
</Row>
)
}
)
}
</div>
</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 : (
<React.Fragment>
{/*<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>*/}
<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>
)
this.state.application?.organizationObj.accountItems?.map(accountItem => {
return (
<React.Fragment key={accountItem.name}>
{
this.renderAccountItem(accountItem)
}
</React.Fragment>
)
})
}
</Card>
)
}
@ -477,7 +587,7 @@ class UserEditPage extends React.Component {
return (
<div>
{
this.state.loading ? <Spin loading={this.state.loading} size="large" /> : (
this.state.loading ? <Spin size="large" /> : (
this.state.user !== null ? this.renderUser() :
<Result
status="404"

View File

@ -49,6 +49,8 @@ import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput";
import BilibiliLoginButton from "./BilibiliLoginButton";
/* eslint-disable jsx-a11y/anchor-is-valid */
class LoginPage extends React.Component {
constructor(props) {
super(props);
@ -144,47 +146,48 @@ class LoginPage extends React.Component {
const application = this.getApplicationObj();
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") {
//cas
const casParams = Util.getCasParameters()
// CAS
const casParams = Util.getCasParameters();
values["type"] = this.state.type;
AuthBackend.loginCas(values, casParams).then((res) => {
if (res.status === 'ok') {
let msg = "Logged in successfully. "
let msg = "Logged in successfully. ";
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.
msg += "Now you can visit apps protected by casdoor."
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
msg += "Now you can visit apps protected by Casdoor.";
}
Util.showMessage("success", msg);
if (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 {
Util.showMessage("error", `Failed to log in: ${res.msg}`);
}
})
} else {
//oauth
// OAuth
const oAuthParams = Util.getOAuthGetParameters();
if (oAuthParams !== null && oAuthParams.responseType != null && oAuthParams.responseType !== "") {
values["type"] = oAuthParams.responseType
}else{
values["type"] = oAuthParams.responseType;
} else {
values["type"] = this.state.type;
}
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
if (oAuthParams !== null){
if (oAuthParams !== null) {
values["samlRequest"] = oAuthParams.samlRequest;
}
if (values["samlRequest"] != null && values["samlRequest"] !== "") {
values["type"] = "saml";
}
AuthBackend.login(values, oAuthParams)
.then((res) => {
if (res.status === 'ok') {
@ -319,7 +322,7 @@ class LoginPage extends React.Component {
)
} else if (provider.category === "SAML") {
return (
<a href="/#" key={provider.displayName} onClick={this.getSamlUrl.bind(this, provider)}>
<a key={provider.displayName} onClick={this.getSamlUrl.bind(this, provider)}>
<img width={width} height={width} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
</a>
)
@ -475,7 +478,7 @@ class LoginPage extends React.Component {
{i18next.t("login:Auto sign in")}
</Checkbox>
</Form.Item>
<a href="/#" style={{float: "right"}} onClick={() => {
<a style={{float: "right"}} onClick={() => {
Setting.goToForget(this, application);
}}>
{i18next.t("login:Forgot password?")}
@ -554,7 +557,7 @@ class LoginPage extends React.Component {
<span style={{float: "left"}}>
{
!application.enableCodeSignin ? null : (
<a href="/#" onClick={() => {
<a onClick={() => {
this.setState({
isCodeSignin: !this.state.isCodeSignin,
});
@ -566,7 +569,7 @@ class LoginPage extends React.Component {
</span>
<span style={{float: "right"}}>
{i18next.t("login:No account?")}&nbsp;
<a href="/#" onClick={() => {
<a onClick={() => {
sessionStorage.setItem("loginURL", window.location.href)
Setting.goToSignup(this, application);
}}>

View File

@ -97,8 +97,8 @@ const authInfo = {
endpoint: "https://appleid.apple.com/auth/authorize",
},
AzureAD: {
scope: "user_impersonation",
endpoint: "https://login.microsoftonline.com/common/oauth2/authorize",
scope: "user.read",
endpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
},
Slack: {
scope: "users:read",
@ -236,7 +236,7 @@ export function getAuthUrl(application, provider, method) {
} else if (provider.type === "Apple") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&response_mode=form_post`;
} else if (provider.type === "AzureAD") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&resource=https://graph.windows.net/`;
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Slack") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Steam") {

View File

@ -25,6 +25,8 @@ import {CountDownInput} from "../common/CountDownInput";
import SelectRegionBox from "../SelectRegionBox";
import CustomGithubCorner from "../CustomGithubCorner";
/* eslint-disable jsx-a11y/anchor-is-valid */
const formItemLayout = {
labelCol: {
xs: {
@ -96,7 +98,10 @@ class SignupPage extends React.Component {
this.setState({
application: application,
});
this.getTermsofuseContent(application.termsOfUse);
if (application !== null && application !== undefined) {
this.getTermsofuseContent(application.termsOfUse);
}
});
}
@ -559,7 +564,7 @@ class SignupPage extends React.Component {
{i18next.t("account:Sign Up")}
</Button>
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp;
<a href="/#" onClick={() => {
<a onClick={() => {
let linkInStorage = sessionStorage.getItem("loginURL")
if(linkInStorage != null){
Setting.goToLink(linkInStorage)

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) {
let formData = new FormData();
formData.append("dest", dest);
@ -124,8 +148,8 @@ export function resetEmailOrPhone(dest, type, code) {
}).then(res => res.json());
}
export function getHumanCheck() {
return fetch(`${Setting.ServerUrl}/api/get-human-check`, {
export function getCaptcha(owner, name, isCurrentProvider) {
return fetch(`${Setting.ServerUrl}/api/get-captcha?applicationId=${owner}/${encodeURIComponent(name)}&isCurrentProvider=${isCurrentProvider}`, {
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 {
return (
<div style={{marginRight:'15px',marginLeft:'15px'}}>
<div style={{marginRight: "15px", marginLeft: "15px"}}>
<Row style={{marginLeft: "-20px", marginRight: "-20px", marginTop: "20px"}} gutter={24}>
{
items.map(item => {

View File

@ -0,0 +1,175 @@
// 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,
subType,
owner,
clientId,
name,
providerUrl,
clientId2,
clientSecret2,
}) => {
const [visible, setVisible] = React.useState(false);
const [captchaImg, setCaptchaImg] = React.useState("");
const [captchaToken, setCaptchaToken] = React.useState("");
const [secret, setSecret] = React.useState(clientSecret);
const [secret2, setSecret2] = React.useState(clientSecret2);
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);
setSecret2(res.clientSecret2);
}
});
};
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 (
<Col>
<Row>
<CaptchaWidget
captchaType={captchaType}
subType={subType}
siteKey={clientId}
clientSecret={secret}
onChange={onSubmit}
clientId2={clientId2}
clientSecret2={secret2}
/>
</Row>
</Col>
);
}
};
const getButtonDisabled = () => {
if (captchaType !== "Default") {
if (!clientId || !clientSecret) {
return true;
}
if (captchaType === "Aliyun Captcha") {
if (!subType || !clientId2 || !clientSecret2) {
return true;
}
}
}
return false;
};
return (
<React.Fragment>
<Button
style={{ fontSize: 14 }}
type={"primary"}
onClick={clickPreview}
disabled={getButtonDisabled()}
>
{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,86 @@
// 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, subType, siteKey, clientSecret, onChange, clientId2, clientSecret2 }) => {
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;
case "Aliyun Captcha":
const AWSCTimer = setInterval(() => {
if (!window.AWSC) {
loadScript("https://g.alicdn.com/AWSC/AWSC/awsc.js");
}
if (window.AWSC) {
if (clientSecret2 && clientSecret2 !== "***") {
window.AWSC.use(subType, function (state, module) {
module.init({
appkey: clientSecret2,
scene: clientId2,
renderTo: "captcha",
success: function (data) {
onChange(`SessionId=${data.sessionId}&AccessKeyId=${siteKey}&Scene=${clientId2}&AppKey=${clientSecret2}&Token=${data.token}&Sig=${data.sig}&RemoteIp=192.168.0.1`);
},
});
});
}
clearInterval(AWSCTimer);
}
}, 300);
break;
default:
break;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2]);
return <div id="captcha"></div>;
};

View File

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

View File

@ -6,7 +6,13 @@
"Sign Up": "Registrieren"
},
"application": {
"Copy SAML metadata URL": "Copy SAML metadata URL",
"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",
"Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable code signin": "Code-Anmeldung aktivieren",
"Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip",
"Enable signin session - Tooltip": "Aktiviere Anmeldesession - Tooltip",
@ -19,21 +25,25 @@
"Password ON": "Passwort AN",
"Password ON - Tooltip": "Whether to allow password login",
"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 URLs": "Umleitungs-URLs",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Aktualisierungs-Token läuft ab",
"Refresh token expire - Tooltip": "Aktualisierungs-Token läuft ab - Tooltip",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
"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",
"Signup items": "Artikel registrieren",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "Test-Nachfrageseite..",
"Test signin page..": "Anmeldeseite testen..",
"Test signup page..": "Anmeldeseite testen..",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Token expire": "Token läuft ab",
"Token expire - Tooltip": "Token läuft ab - Tooltip",
"Token format": "Token-Format",
"Token format - Tooltip": "Token-Format - Tooltip"
"Token format - Tooltip": "Token-Format - Tooltip",
"rule": "rule"
},
"cert": {
"Bit size": "Bitgröße",
@ -99,6 +109,7 @@
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "Client-IP",
"Created time": "Erstellte Zeit",
"Default avatar": "Standard Avatar",
@ -131,6 +142,9 @@
"Master password": "Master-Passwort",
"Master password - Tooltip": "Masterpasswort - Tooltip",
"Method": "Methode",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Name",
"Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "OAuth-Anbieter",
@ -243,7 +257,15 @@
"sign up now": "jetzt anmelden",
"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": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Standard Avatar",
"Edit Organization": "Organisation bearbeiten",
"Favicon": "Févicon",
@ -255,7 +277,9 @@
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website-URL",
"Website URL - Tooltip": "Unique string-style identifier"
"Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
},
"payment": {
"Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +411,7 @@
"Domain": "Domäne",
"Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "Anbieter bearbeiten",
"Email Content": "Email Content",
"Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "E-Mail-Titel",
"Email Title - Tooltip": "Unique string-style identifier",
@ -422,10 +446,15 @@
"SP ACS URL": "SP-ACS-URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Geheimer Zugangsschlüssel",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
"Send Test Email": "Send Test Email",
"Sign Name": "Schild Name",
"Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Signaturanfrage",
@ -436,12 +465,17 @@
"Signup HTML": "HTML registrieren",
"Signup HTML - Edit": "HTML registrieren - Bearbeiten",
"Signup HTML - Tooltip": "HTML registrieren - Tooltip",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Vorlagencode",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Nutzungsbedingungen",
"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 - Tooltip": "Token URL - Tooltip",
"Type": "Typ",
@ -454,7 +488,6 @@
"canUnlink": "canUnlink",
"prompted": "gefragt",
"required": "benötigt",
"rule": "regel",
"visible": "sichtbar"
},
"record": {
@ -483,6 +516,7 @@
},
"signup": {
"Accept": "Akzeptieren",
"Agreement": "Agreement",
"Confirm": "Bestätigen",
"Decline": "Ablehnen",
"Have account?": "Haben Sie Konto?",
@ -505,7 +539,6 @@
"The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "Die Eingabe ist ungültig!",
"The input is not valid Phone!": "Die Eingabe ist nicht gültig!",
"Unknown Check Type": "Unbekannter Schecktyp",
"Username": "Benutzername",
"Username - Tooltip": "Benutzername - Tooltip",
"Your account has been created!": "Ihr Konto wurde erstellt!",
@ -558,6 +591,8 @@
"Bio": "Bio",
"Bio - Tooltip": "Bio - Tooltip",
"Cancel": "Abbrechen",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code gesendet",
"Country/Region": "Land/Region",
"Country/Region - Tooltip": "Country/Region",

View File

@ -6,7 +6,13 @@
"Sign Up": "Sign Up"
},
"application": {
"Copy SAML metadata URL": "Copy SAML metadata URL",
"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 SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable code signin": "Enable code signin",
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
@ -19,21 +25,25 @@
"Password ON": "Password ON",
"Password ON - Tooltip": "Password ON - Tooltip",
"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 URLs": "Redirect URLs",
"Redirect URLs - Tooltip": "Redirect URLs - Tooltip",
"Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "Signin session",
"Signup items": "Signup items",
"Signup items - Tooltip": "Signup items - Tooltip",
"Test prompt page..": "Test prompt page..",
"Test signin page..": "Test signin page..",
"Test signup page..": "Test signup 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",
"Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip"
"Token format - Tooltip": "Token format - Tooltip",
"rule": "rule"
},
"cert": {
"Bit size": "Bit size",
@ -99,6 +109,7 @@
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Created time": "Created time",
"Default avatar": "Default avatar",
@ -131,6 +142,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": "Name - Tooltip",
"OAuth providers": "OAuth providers",
@ -243,7 +257,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": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
@ -255,7 +277,9 @@
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL",
"Website URL - Tooltip": "Website URL - Tooltip"
"Website URL - Tooltip": "Website URL - Tooltip",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
},
"payment": {
"Confirm your invoice information": "Confirm your invoice information",
@ -422,10 +446,15 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Secret access key",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
"Send Test Email": "Send Test Email",
"Sign Name": "Sign Name",
"Sign Name - Tooltip": "Sign Name - Tooltip",
"Sign request": "Sign request",
@ -436,12 +465,17 @@
"Signup HTML": "Signup HTML",
"Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Signup HTML - Tooltip",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Template Code",
"Template Code - Tooltip": "Template Code - Tooltip",
"Terms of Use": "Terms of Use",
"Terms of Use - Tooltip": "Terms of Use - Tooltip",
"Test Connection": "Test Connection",
"Test Email": "Test Email",
"Test Email - Tooltip": "Test Email - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Type",
@ -454,7 +488,6 @@
"canUnlink": "canUnlink",
"prompted": "prompted",
"required": "required",
"rule": "rule",
"visible": "visible"
},
"record": {
@ -483,6 +516,7 @@
},
"signup": {
"Accept": "Accept",
"Agreement": "Agreement",
"Confirm": "Confirm",
"Decline": "Decline",
"Have account?": "Have account?",
@ -505,7 +539,6 @@
"The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "The input is not valid Email!",
"The input is not valid Phone!": "The input is not valid Phone!",
"Unknown Check Type": "Unknown Check Type",
"Username": "Username",
"Username - Tooltip": "Username - Tooltip",
"Your account has been created!": "Your account has been created!",
@ -558,6 +591,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 - Tooltip",

View File

@ -6,7 +6,13 @@
"Sign Up": "S'inscrire"
},
"application": {
"Copy SAML metadata URL": "Copy SAML metadata URL",
"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",
"Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable code signin": "Activer la connexion au code",
"Enable code signin - Tooltip": "Activer la connexion au code - infobulle",
"Enable signin session - Tooltip": "Activer la session de connexion - infobulle",
@ -19,21 +25,25 @@
"Password ON": "Mot de passe activé",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML",
"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 URLs": "URL de redirection",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Expiration du jeton d'actualisation",
"Refresh token expire - Tooltip": "Expiration du jeton d'actualisation - infobulle",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
"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",
"Signup items": "Inscrire des éléments",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "Tester la vitesse d'exécution.",
"Test signin page..": "Tester la connexion en ligne.",
"Test signup page..": "Tester l'inscription.",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Token expire": "Expiration du jeton",
"Token expire - Tooltip": "Expiration du jeton - Info-bulle",
"Token format": "Format du jeton",
"Token format - Tooltip": "Format du jeton - infobulle"
"Token format - Tooltip": "Format du jeton - infobulle",
"rule": "rule"
},
"cert": {
"Bit size": "Taille du bit",
@ -99,6 +109,7 @@
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certes",
"Click to Upload": "Click to Upload",
"Client IP": "IP du client",
"Created time": "Date de création",
"Default avatar": "Avatar par défaut",
@ -131,6 +142,9 @@
"Master password": "Mot de passe maître",
"Master password - Tooltip": "Mot de passe maître - Infobulle",
"Method": "Méthode",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Nom",
"Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "Fournisseurs OAuth",
@ -243,7 +257,15 @@
"sign up now": "inscrivez-vous maintenant",
"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": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Avatar par défaut",
"Edit Organization": "Modifier l'organisation",
"Favicon": "Favicon",
@ -255,7 +277,9 @@
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"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": {
"Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +411,7 @@
"Domain": "Domaine",
"Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "Modifier le fournisseur",
"Email Content": "Email Content",
"Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "Titre de l'e-mail",
"Email Title - Tooltip": "Unique string-style identifier",
@ -422,10 +446,15 @@
"SP ACS URL": "URL du SP ACS",
"SP ACS URL - Tooltip": "URL SP ACS - infobulle",
"SP Entity ID": "ID de l'entité SP",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Clé d'accès secrète",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Infobulle",
"Send Test Email": "Send Test Email",
"Sign Name": "Nom du panneau",
"Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Demande de signature",
@ -436,12 +465,17 @@
"Signup HTML": "Inscription HTML",
"Signup HTML - Edit": "Inscription HTML - Modifier",
"Signup HTML - Tooltip": "Inscription HTML - infobulle",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Code du modèle",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Conditions d'utilisation",
"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 - Tooltip": "Token URL - Tooltip",
"Type": "Type de texte",
@ -454,7 +488,6 @@
"canUnlink": "canUnlink",
"prompted": "invitée",
"required": "Obligatoire",
"rule": "règle",
"visible": "Visible"
},
"record": {
@ -483,6 +516,7 @@
},
"signup": {
"Accept": "Accepter",
"Agreement": "Agreement",
"Confirm": "Valider",
"Decline": "Refuser",
"Have account?": "Vous avez un compte ?",
@ -505,7 +539,6 @@
"The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "L'entrée n'est pas un email valide !",
"The input is not valid Phone!": "L'entrée n'est pas un téléphone valide !",
"Unknown Check Type": "Type de vérification inconnu",
"Username": "Nom d'utilisateur",
"Username - Tooltip": "Nom d'utilisateur - Info-bulle",
"Your account has been created!": "Votre compte a été créé !",
@ -558,6 +591,8 @@
"Bio": "Bio",
"Bio - Tooltip": "Bio - Infobulle",
"Cancel": "Abandonner",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code envoyé",
"Country/Region": "Pays/Région",
"Country/Region - Tooltip": "Country/Region",

View File

@ -6,7 +6,13 @@
"Sign Up": "新規登録"
},
"application": {
"Copy SAML metadata URL": "Copy SAML metadata URL",
"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": "アプリケーションを編集",
"Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable code signin": "コードサインインを有効にする",
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
@ -19,21 +25,25 @@
"Password ON": "パスワードON",
"Password ON - Tooltip": "Whether to allow password login",
"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 URLs": "リダイレクトURL",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "トークンの更新の期限が切れます",
"Refresh token expire - Tooltip": "トークンの有効期限を更新する - ツールチップ",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
"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": "サインインセッション",
"Signup items": "アイテムの登録",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "テストプロンプトページ...",
"Test signin page..": "サインインテストページ...",
"Test signup 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",
"Token expire": "トークンの有効期限",
"Token expire - Tooltip": "トークンの有効期限 - ツールチップ",
"Token format": "トークンのフォーマット",
"Token format - Tooltip": "トークンフォーマット - ツールチップ"
"Token format - Tooltip": "トークンフォーマット - ツールチップ",
"rule": "rule"
},
"cert": {
"Bit size": "ビットサイズ",
@ -99,6 +109,7 @@
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "クライアント IP",
"Created time": "作成日時",
"Default avatar": "デフォルトのアバター",
@ -131,6 +142,9 @@
"Master password": "マスターパスワード",
"Master password - Tooltip": "マスターパスワード - ツールチップ",
"Method": "方法",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "名前",
"Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "OAuthプロバイダー",
@ -243,7 +257,15 @@
"sign up now": "今すぐサインアップ",
"username, Email or phone": "ユーザー名、メールアドレスまたは電話番号"
},
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "デフォルトのアバター",
"Edit Organization": "組織を編集",
"Favicon": "ファビコン",
@ -255,7 +277,9 @@
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL",
"Website URL - Tooltip": "Unique string-style identifier"
"Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
},
"payment": {
"Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +411,7 @@
"Domain": "ドメイン",
"Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "プロバイダーを編集",
"Email Content": "Email Content",
"Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "メールタイトル",
"Email Title - Tooltip": "Unique string-style identifier",
@ -422,10 +446,15 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - ツールチップ",
"SP Entity ID": "SP ID",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "シークレットアクセスキー",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "シークレットアクセスキー - ツールチップ",
"Send Test Email": "Send Test Email",
"Sign Name": "署名名",
"Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "サインリクエスト",
@ -436,12 +465,17 @@
"Signup HTML": "HTMLの登録",
"Signup HTML - Edit": "HTMLの登録 - 編集",
"Signup HTML - Tooltip": "サインアップ HTML - ツールチップ",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "テンプレートコード",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "利用規約",
"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 - Tooltip": "Token URL - Tooltip",
"Type": "タイプ",
@ -454,7 +488,6 @@
"canUnlink": "canUnlink",
"prompted": "プロンプトされた",
"required": "必須",
"rule": "ルール",
"visible": "表示"
},
"record": {
@ -483,6 +516,7 @@
},
"signup": {
"Accept": "同意する",
"Agreement": "Agreement",
"Confirm": "確認する",
"Decline": "同意しない",
"Have account?": "アカウントをお持ちですか?",
@ -505,7 +539,6 @@
"The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "入力されたメールアドレスが無効です!",
"The input is not valid Phone!": "入力された電話番号が正しくありません!",
"Unknown Check Type": "不明なチェックタイプ",
"Username": "ユーザー名",
"Username - Tooltip": "ユーザー名 - ツールチップ",
"Your account has been created!": "あなたのアカウントが作成されました!",
@ -558,6 +591,8 @@
"Bio": "略歴",
"Bio - Tooltip": "バイオチップ(ツールチップ)",
"Cancel": "キャンセル",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "コードを送信しました",
"Country/Region": "国/地域",
"Country/Region - Tooltip": "Country/Region",

View File

@ -6,7 +6,13 @@
"Sign Up": "Sign Up"
},
"application": {
"Copy SAML metadata URL": "Copy SAML metadata URL",
"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 SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable code signin": "Enable code signin",
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
@ -19,21 +25,25 @@
"Password ON": "Password ON",
"Password ON - Tooltip": "Whether to allow password login",
"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 URLs": "Redirect URLs",
"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",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
"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",
"Test prompt page..": "Test prompt page..",
"Test signin page..": "Test signin page..",
"Test signup page..": "Test signup 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",
"Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip"
"Token format - Tooltip": "Token format - Tooltip",
"rule": "rule"
},
"cert": {
"Bit size": "Bit size",
@ -99,6 +109,7 @@
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
"Certs": "Certs",
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Created time": "Created time",
"Default avatar": "Default avatar",
@ -131,6 +142,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",
"OAuth providers": "OAuth providers",
@ -243,7 +257,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": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
@ -255,7 +277,9 @@
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL",
"Website URL - Tooltip": "Unique string-style identifier"
"Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
},
"payment": {
"Confirm your invoice information": "Confirm your invoice information",
@ -387,9 +411,9 @@
"Domain": "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 Title": "Email Title",
"Email Title": "Email title",
"Email Title - Tooltip": "Unique string-style identifier",
"Endpoint": "Endpoint",
"Endpoint (Intranet)": "Endpoint (Intranet)",
@ -422,10 +446,15 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Secret access key",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
"Send Test Email": "Send Test Email",
"Sign Name": "Sign Name",
"Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Sign request",
@ -436,12 +465,17 @@
"Signup HTML": "Signup HTML",
"Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Signup HTML - Tooltip",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Template Code",
"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 - Tooltip": "Token URL - Tooltip",
"Type": "Type",
@ -454,7 +488,6 @@
"canUnlink": "canUnlink",
"prompted": "prompted",
"required": "required",
"rule": "rule",
"visible": "visible"
},
"record": {
@ -483,6 +516,7 @@
},
"signup": {
"Accept": "Accept",
"Agreement": "Agreement",
"Confirm": "Confirm",
"Decline": "Decline",
"Have account?": "Have account?",
@ -505,7 +539,6 @@
"The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "The input is not valid Email!",
"The input is not valid Phone!": "The input is not valid Phone!",
"Unknown Check Type": "Unknown Check Type",
"Username": "Username",
"Username - Tooltip": "Username - Tooltip",
"Your account has been created!": "Your account has been created!",
@ -558,6 +591,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",

View File

@ -6,7 +6,13 @@
"Sign Up": "Регистрация"
},
"application": {
"Copy SAML metadata URL": "Copy SAML metadata URL",
"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": "Изменить приложение",
"Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable code signin": "Включить кодовый вход",
"Enable code signin - Tooltip": "Включить вход с кодом - Tooltip",
"Enable signin session - Tooltip": "Включить сеанс входа - Подсказка",
@ -19,21 +25,25 @@
"Password ON": "Пароль ВКЛ",
"Password ON - Tooltip": "Whether to allow password login",
"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 URLs": "Перенаправление URL",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Срок действия обновления токена истекает",
"Refresh token expire - Tooltip": "Срок обновления токена истекает - Подсказка",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
"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": "Сессия входа",
"Signup items": "Элементы регистрации",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Test prompt page..": "Тестовая страница запроса..",
"Test signin page..": "Тестовая страница входа..",
"Test signup 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",
"Token expire": "Токен истекает",
"Token expire - Tooltip": "Истек токен - Подсказка",
"Token format": "Формат токена",
"Token format - Tooltip": "Формат токена - Подсказка"
"Token format - Tooltip": "Формат токена - Подсказка",
"rule": "rule"
},
"cert": {
"Bit size": "Размер бита",
@ -99,6 +109,7 @@
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
"Certs": "Сертификаты",
"Click to Upload": "Click to Upload",
"Client IP": "IP клиента",
"Created time": "Время создания",
"Default avatar": "Аватар по умолчанию",
@ -131,6 +142,9 @@
"Master password": "Мастер-пароль",
"Master password - Tooltip": "Мастер-пароль - Tooltip",
"Method": "Метод",
"Model": "Model",
"Model - Tooltip": "Model - Tooltip",
"Models": "Models",
"Name": "Наименование",
"Name - Tooltip": "Unique string-style identifier",
"OAuth providers": "Поставщики OAuth",
@ -243,7 +257,15 @@
"sign up now": "зарегистрироваться",
"username, Email or phone": "имя пользователя, адрес электронной почты или телефон"
},
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
"Model text - Tooltip": "Model text - Tooltip",
"New Model": "New Model"
},
"organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Default avatar": "Аватар по умолчанию",
"Edit Organization": "Изменить организацию",
"Favicon": "Иконка",
@ -255,7 +277,9 @@
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "URL сайта",
"Website URL - Tooltip": "Unique string-style identifier"
"Website URL - Tooltip": "Unique string-style identifier",
"modifyRule": "modifyRule",
"viewRule": "viewRule"
},
"payment": {
"Confirm your invoice information": "Confirm your invoice information",
@ -387,7 +411,7 @@
"Domain": "Домен",
"Domain - Tooltip": "Storage endpoint custom domain",
"Edit Provider": "Изменить провайдера",
"Email Content": "Email Content",
"Email Content": "Email content",
"Email Content - Tooltip": "Unique string-style identifier",
"Email Title": "Заголовок письма",
"Email Title - Tooltip": "Unique string-style identifier",
@ -422,10 +446,15 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Подсказка",
"SP Entity ID": "Идентификатор сущности SP",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Секретный ключ доступа",
"Secret key": "Secret key",
"Secret key - Tooltip": "Secret key - Tooltip",
"SecretAccessKey - Tooltip": "SecretAccessKey - Подсказка",
"Send Test Email": "Send Test Email",
"Sign Name": "Имя подписи",
"Sign Name - Tooltip": "Unique string-style identifier",
"Sign request": "Запрос на подпись",
@ -436,12 +465,17 @@
"Signup HTML": "Регистрация HTML",
"Signup HTML - Edit": "Регистрация HTML - Редактировать",
"Signup HTML - Tooltip": "Регистрация HTML - Подсказка",
"Site key": "Site key",
"Site key - Tooltip": "Site key - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Код шаблона",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Условия использования",
"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 - Tooltip": "Token URL - Tooltip",
"Type": "Тип",
@ -454,7 +488,6 @@
"canUnlink": "canUnlink",
"prompted": "запрошено",
"required": "обязательный",
"rule": "правило",
"visible": "видимый"
},
"record": {
@ -483,6 +516,7 @@
},
"signup": {
"Accept": "Принять",
"Agreement": "Agreement",
"Confirm": "Подтвердить",
"Decline": "Отклонить",
"Have account?": "Есть аккаунт?",
@ -505,7 +539,6 @@
"The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "Ввод не является допустимым Email!",
"The input is not valid Phone!": "Введен неверный телефон!",
"Unknown Check Type": "Неизвестный тип проверки",
"Username": "Имя пользователя",
"Username - Tooltip": "Имя пользователя - Подсказка",
"Your account has been created!": "Ваша учетная запись была создана!",
@ -558,6 +591,8 @@
"Bio": "Био",
"Bio - Tooltip": "Био - Подсказка",
"Cancel": "Отмена",
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Код отправлен",
"Country/Region": "Страна/регион",
"Country/Region - Tooltip": "Country/Region",

View File

@ -6,7 +6,13 @@
"Sign Up": "注册"
},
"application": {
"Copy SAML metadata URL": "复制SAML元数据URL",
"Copy prompt page URL": "复制提醒页面URL",
"Copy signin page URL": "复制登录页面URL",
"Copy signup page URL": "复制注册页面URL",
"Edit Application": "编辑应用",
"Enable SAML compress": "压缩SAML响应",
"Enable SAML compress - Tooltip": "Casdoor作为SAML idp时是否压缩SAML响应信息",
"Enable code signin": "启用验证码登录",
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
"Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话",
@ -19,21 +25,25 @@
"Password ON": "开启密码",
"Password ON - Tooltip": "是否允许密码登录",
"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 URLs": "重定向 URLs",
"Redirect URLs - Tooltip": "登录成功后重定向地址列表",
"Refresh token expire": "Refresh Token过期时间",
"Refresh token expire": "Refresh Token过期",
"Refresh token expire - Tooltip": "Refresh Token过期时间",
"SAML metadata": "SAML元数据",
"SAML metadata - Tooltip": "SAML协议的元数据Metadata信息",
"SAML metadata URL copied to clipboard successfully": "SAML元数据URL已成功复制到剪贴板",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "登录页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Signin session": "保持登录会话",
"Signup items": "注册项",
"Signup items - Tooltip": "注册用户注册时需要填写的项目",
"Test prompt page..": "测试提醒页面..",
"Test signin page..": "测试登录页面..",
"Test signup page..": "测试注册页面..",
"Token expire": "Access Token过期时间",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "注册页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Token expire": "Access Token过期",
"Token expire - Tooltip": "Access Token过期时间",
"Token format": "Access Token格式",
"Token format - Tooltip": "Access Token格式"
"Token format - Tooltip": "Access Token格式",
"rule": "规则"
},
"cert": {
"Bit size": "位大小",
@ -99,6 +109,7 @@
"Cert": "证书",
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
"Certs": "证书",
"Click to Upload": "点击上传",
"Client IP": "客户端IP",
"Created time": "创建时间",
"Default avatar": "默认头像",
@ -131,6 +142,8 @@
"Master password": "万能密码",
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
"Method": "方法",
"Model": "模型",
"Model - Tooltip": "Casbin模型",
"Models": "模型",
"Name": "名称",
"Name - Tooltip": "唯一的、字符串式的ID",
@ -246,13 +259,17 @@
},
"model": {
"Edit Model": "编辑模型",
"Model": "模型"
"Model text": "模型文本",
"Model text - Tooltip": "Casbin访问控制模型",
"New Model": "添加模型"
},
"organization": {
"Account items": "个人页设置项",
"Account items - Tooltip": "个人设置页面中的项目",
"Default avatar": "默认头像",
"Edit Organization": "编辑组织",
"Favicon": "图标",
"Is profile public": "公开用户主页",
"Is profile public": "用户个人页公开",
"Is profile public - Tooltip": "关闭后,只有全局管理员或同组织用户才能访问用户主页",
"New Organization": "添加组织",
"Soft deletion": "软删除",
@ -260,7 +277,9 @@
"Tags": "标签集合",
"Tags - Tooltip": "可供用户选择的标签的集合",
"Website URL": "网页地址",
"Website URL - Tooltip": "网页地址"
"Website URL - Tooltip": "网页地址",
"modifyRule": "修改规则",
"viewRule": "查看规则"
},
"payment": {
"Confirm your invoice information": "确认您的发票信息",
@ -404,7 +423,7 @@
"IdP public key": "IdP 公钥",
"Issuer URL": "发行者网址",
"Issuer URL - Tooltip": "发行者URL - 工具提示",
"Link copied to clipboard successfully": "链接复制到剪贴板成功",
"Link copied to clipboard successfully": "链接已成功复制到剪贴板",
"Metadata": "元数据",
"Metadata - Tooltip": "元数据 - 工具提示",
"Method": "方法",
@ -427,26 +446,36 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - 工具提示",
"SP Entity ID": "SP 实体 ID",
"Scene": "Scene",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope - 工具提示",
"Secret access key": "秘密访问密钥",
"Secret key": "Secret key",
"Secret key - Tooltip": "用于服务端调用验证码提供商API进行验证",
"SecretAccessKey - Tooltip": "访问密钥-工具提示",
"Send Test Email": "发送测试邮件",
"Sign Name": "签名名称",
"Sign Name - Tooltip": "签名名称",
"Sign request": "签名请求",
"Sign request - Tooltip": "签名请求 - 工具提示",
"Signin HTML": "登录 HTML",
"Signin HTML - Edit": "登录 HTML - 编辑",
"Signin HTML - Tooltip": "登录 HTML - 工具提示",
"Signup HTML": "注册 HTML",
"Signup HTML - Edit": "注册 HTML - 编辑",
"Signup HTML - Tooltip": "注册 HTML - 工具提示",
"Signin HTML": "登录页面HTML",
"Signin HTML - Edit": "登录页面 - 编辑",
"Signin HTML - Tooltip": "自定义HTML,用于替换默认的登录页面样式",
"Signup HTML": "注册页面HTML",
"Signup HTML - Edit": "注册页面HTML - 编辑",
"Signup HTML - Tooltip": "自定义HTML,用于替换默认的注册页面样式",
"Site key": "Site key",
"Site key - Tooltip": "用于前端嵌入页面",
"Sub type": "子类型",
"Sub type - Tooltip": "子类型",
"Template Code": "模板CODE",
"Template Code - Tooltip": "模板CODE",
"Template Code": "模板代码",
"Template Code - Tooltip": "模板代码",
"Terms of Use": "使用条款",
"Terms of Use - Tooltip": "使用条款 - 工具提示",
"Test Connection": "测试Smtp连接",
"Test Email": "测试Email配置",
"Test Email - Tooltip": "邮箱地址",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - 工具提示",
"Type": "类型",
@ -454,13 +483,12 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - 工具提示",
"alertType": "警报类型",
"canSignIn": "canSignIn",
"canSignUp": "canSignUp",
"canUnlink": "canUnlink",
"prompted": "提示",
"required": "必需",
"rule": "规则",
"visible": "可见"
"canSignIn": "可用于登录",
"canSignUp": "可用于注册",
"canUnlink": "可解绑定",
"prompted": "注册后提醒绑定",
"required": "是否必填项",
"visible": "是否可见"
},
"record": {
"Is Triggered": "已触发"
@ -488,6 +516,7 @@
},
"signup": {
"Accept": "阅读并接受",
"Agreement": "用户协议",
"Confirm": "确认密码",
"Decline": "不接受",
"Have account?": "已有账号?",
@ -510,7 +539,6 @@
"The input is not invoice title!": "您输入的发票抬头有误!",
"The input is not valid Email!": "您输入的电子邮箱格式有误!",
"The input is not valid Phone!": "您输入的手机号格式有误!",
"Unknown Check Type": "未知的验证码类型",
"Username": "用户名",
"Username - Tooltip": "允许字符包括字母、数字、下划线,不得以数字开头",
"Your account has been created!": "您的账号已创建!",
@ -563,6 +591,8 @@
"Bio": "自我介绍",
"Bio - Tooltip": "自我介绍",
"Cancel": "取消",
"Captcha Verify Failed": "验证码校验失败",
"Captcha Verify Success": "验证码校验成功",
"Code Sent": "验证码已发送",
"Country/Region": "国家/地区",
"Country/Region - Tooltip": "国家/地区",