Compare commits

...

66 Commits

Author SHA1 Message Date
afa9c530ad fix: panic triggered when user is nil (#940) 2022-07-31 23:23:36 +08:00
1600615aca Support sqlite3 DB 2022-07-31 18:11:18 +08:00
2bb8491499 fix: unable to get user if profile is private (#936) 2022-07-31 10:54:41 +08:00
293283ed25 feat: add get user by phone (#934)
* fix: check reset phone & email modify rules

* Update verification.go

* Update organization.go

* feat: add get user by phone

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-31 01:02:28 +08:00
9cb519d1e9 fix: Admins should not be allowed to add third-party login for their members (#932)
* feat: admin can unlink the other user

* feat: global admin can unlink other user

* fix
2022-07-30 23:11:02 +08:00
fb9b8f1662 fix: skip the duplicated users when sync users (#928)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-30 22:24:23 +08:00
2fec3f72ae fix: check reset phone & email modify rules (#927)
* fix: check reset phone & email modify rules

* Update verification.go

* Update organization.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-30 18:17:13 +08:00
11695220a8 Use user.GetId() 2022-07-30 17:40:30 +08:00
155660b0d7 feat: get user api return roles and permissions (#929) 2022-07-30 17:31:56 +08:00
1c72f5300c feat: fix 'Enable code sign' is not displayed in the login page (#925) 2022-07-28 23:11:33 +08:00
3dd56195d9 fix: fix the problem of link error (#923) 2022-07-28 21:52:10 +08:00
8865244262 fix: add oauth login auto close page (#915) 2022-07-26 23:03:55 +08:00
3400fa1e9c feat: support local login for non-built-in users (#911)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-26 19:27:24 +08:00
bdc5c92ef0 fix: send code missing parameter & show more detail responseError (#910) 2022-07-25 23:46:38 +08:00
4e3eedf246 feat: fix bug that the default permission prevents admin to login in (#907)
* fix:The certs page is displayed incorrectly

* Translations for each language are added

* Replace the variables certificat with Certificat with certificate and Certificate

* Replace the variables certificat with Certificat with certificate and Certificate

* Variable names are more accurate

* Variable names are more accurate

* Modify the variable name

* fix: Default action prevents admin to login in
2022-07-24 23:36:55 +08:00
8e98fc5a9f feat: rename all publicKey occurrences to certificate (#894)
* fix:The certs page is displayed incorrectly

* Translations for each language are added

* Replace the variables certificat with Certificat with certificate and Certificate

* Replace the variables certificat with Certificat with certificate and Certificate

* Variable names are more accurate

* Variable names are more accurate

* Modify the variable name
2022-07-23 09:40:51 +08:00
6f6159be07 feat: add GET method of logout API (#903) 2022-07-22 21:13:49 +08:00
3e4dbc2dcb fix: URL bug in getUploadFileUrl function 2022-07-20 17:49:11 +08:00
48b5b27982 fix: invalid redirect url after sign up (#896)
* fix: invalid redirect url after sign up

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

* Update App.js

* Update Setting.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-19 23:31:17 +08:00
1839252c30 chore(web): sort import members (#895) 2022-07-18 20:57:38 +08:00
1fff1db6a7 fix(web): fix the bug of infinity loop animate when unauthorized (#891)
* fix(web): fix the bug of infinity loop when unauthorized

* fix

* fix

* fix

* Update BaseListPage.js

* Update OrganizationListPage.js

* Update OrganizationListPage.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-17 18:20:52 +08:00
a0b0e186b7 Improve i18n code and data. 2022-07-17 17:56:43 +08:00
8c7f235ee1 Fix bug in uploadFile()'s URL. 2022-07-17 14:29:06 +08:00
a0a762aa6f fix: typo in field tag in BilibiliUserInfo (#890) 2022-07-17 11:31:43 +08:00
2eec53a6d0 fix: actions initialized to null and model/resources not updated with the owner (#887)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-16 15:00:42 +08:00
117dec4542 feat: failed to sync keycloak users in the PostgreSQL database (#886)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-16 12:14:35 +08:00
895cdd024d fix: Typo in user model xorm tag (#883) 2022-07-15 12:01:27 +08:00
f0b0891ac9 feat: query user by userId (#879)
* feat: add `getUserByUserId` func

* Update user.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-14 21:46:13 +08:00
10449e89ab Fix owner bug in GetUser(). 2022-07-13 22:56:35 +08:00
6e70f0fc58 Refactor CheckAccessPermission(). 2022-07-13 00:50:32 +08:00
2bca424370 feat: implement access control using casbin (#806)
* feat: implement access control using casbin

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

* chore: sort imports

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

* fix: remove

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

* Update auth.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-07-13 00:34:35 +08:00
de49a45e19 Add escapePath for getUploadFileUrl(). 2022-07-12 23:24:24 +08:00
f7243f879b Fix some JS warnings. 2022-07-12 20:47:11 +08:00
7f3b2500b3 feat: support webauthn (#407)
* feat: support webauthn

* Update init.go

* Update user_webauthn.go

* Update UserEditPage.js

* Update WebauthnCredentialTable.js

* Update LoginPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-07-12 20:06:01 +08:00
208dc11d25 fix: set SessionOn always true (#877)
* fix: set SessionOn always true

* Update adapter.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-07-11 12:36:20 +08:00
503d244166 feat(web): add lint (#875)
* feat: add lint

* feat: fix lint error

* chore: add ignore file

* chore: close indent
2022-07-10 15:45:55 +08:00
475b6da35a Rename session storage item to signinUrl. 2022-07-10 11:50:48 +08:00
b9404f14dc feat: fix bug of using email provider from wrong application (#869) 2022-07-10 00:40:52 +08:00
0baae87390 feat: fix oauth unknown authority in docker (#871) 2022-07-09 17:36:56 +08:00
06759041a8 Fix socks5Proxy config typo. 2022-07-08 23:24:54 +08:00
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
200 changed files with 19955 additions and 17277 deletions

View File

@ -114,6 +114,7 @@ jobs:
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true' if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
with: with:
target: STANDARD
push: true push: true
tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest 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 FROM node:16.13.0 AS FRONT
WORKDIR /web WORKDIR /web
COPY ./web . COPY ./web .
@ -10,28 +5,47 @@ RUN yarn config set registry https://registry.npmmirror.com
RUN yarn install && yarn run build RUN yarn install && yarn run build
FROM debian:latest AS ALLINONE FROM golang:1.17.5 AS BACK
RUN apt update WORKDIR /go/src/casdoor
RUN apt install -y ca-certificates && update-ca-certificates COPY . .
RUN apt install -y mariadb-server mariadb-client && mkdir -p web/build && chmod 777 /tmp RUN ./build.sh
FROM alpine:latest AS STANDARD
LABEL MAINTAINER="https://casdoor.org/" 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 alpine:latest
RUN sed -i 's/https/http/' /etc/apk/repositories RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add curl RUN apk add curl
RUN apk add ca-certificates && update-ca-certificates RUN apk add ca-certificates && update-ca-certificates
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/" LABEL MAINTAINER="https://casdoor.org/"
COPY --from=BACK /go/src/casdoor/ ./ RUN apt update
COPY --from=BACK /usr/bin/wait-for-it ./ RUN apt install -y ca-certificates && update-ca-certificates
RUN mkdir -p web/build && apk add --no-cache bash coreutils
COPY --from=FRONT /web/build /web/build WORKDIR /
CMD ./server 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 ### 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

@ -78,6 +78,7 @@ p, *, *, POST, /api/get-email-and-phone, *, *
p, *, *, POST, /api/login, *, * p, *, *, POST, /api/login, *, *
p, *, *, GET, /api/get-app-login, *, * p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, * p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/logout, *, *
p, *, *, GET, /api/get-account, *, * p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, * p, *, *, GET, /api/userinfo, *, *
p, *, *, *, /api/login/oauth, *, * p, *, *, *, /api/login/oauth, *, *
@ -92,6 +93,7 @@ p, *, *, GET, /api/get-payment, *, *
p, *, *, POST, /api/update-payment, *, * p, *, *, POST, /api/update-payment, *, *
p, *, *, POST, /api/invoice-payment, *, * p, *, *, POST, /api/invoice-payment, *, *
p, *, *, GET, /api/get-providers, *, * p, *, *, GET, /api/get-providers, *, *
p, *, *, POST, /api/notify-payment, *, *
p, *, *, POST, /api/unlink, *, * p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, * p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, * p, *, *, POST, /api/send-verification-code, *, *
@ -105,6 +107,7 @@ p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, * p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, * p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, * p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, *
` `
sa := stringadapter.NewAdapter(ruleText) sa := stringadapter.NewAdapter(ruleText)

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
}

View File

@ -16,9 +16,11 @@ package captcha
import ( import (
"encoding/json" "encoding/json"
"errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strings"
) )
const HCaptchaVerifyUrl = "https://hcaptcha.com/siteverify" const HCaptchaVerifyUrl = "https://hcaptcha.com/siteverify"
@ -49,6 +51,7 @@ func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool
type captchaResponse struct { type captchaResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
ErrorCodes []string `json:"error-codes"`
} }
captchaResp := &captchaResponse{} captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp) err = json.Unmarshal(body, captchaResp)
@ -56,5 +59,9 @@ func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool
return false, err return false, err
} }
if len(captchaResp.ErrorCodes) > 0 {
return false, errors.New(strings.Join(captchaResp.ErrorCodes, ","))
}
return captchaResp.Success, nil return captchaResp.Success, nil
} }

View File

@ -25,6 +25,8 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
return NewReCaptchaProvider() return NewReCaptchaProvider()
} else if captchaType == "hCaptcha" { } else if captchaType == "hCaptcha" {
return NewHCaptchaProvider() return NewHCaptchaProvider()
} else if captchaType == "Aliyun Captcha" {
return NewAliyunCaptchaProvider()
} }
return nil return nil
} }

View File

@ -16,9 +16,11 @@ package captcha
import ( import (
"encoding/json" "encoding/json"
"errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strings"
) )
const ReCaptchaVerifyUrl = "https://recaptcha.net/recaptcha/api/siteverify" const ReCaptchaVerifyUrl = "https://recaptcha.net/recaptcha/api/siteverify"
@ -49,6 +51,7 @@ func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientSecret string) (boo
type captchaResponse struct { type captchaResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
ErrorCodes []string `json:"error-codes"`
} }
captchaResp := &captchaResponse{} captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp) err = json.Unmarshal(body, captchaResp)
@ -56,5 +59,9 @@ func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientSecret string) (boo
return false, err return false, err
} }
if len(captchaResp.ErrorCodes) > 0 {
return false, errors.New(strings.Join(captchaResp.ErrorCodes, ","))
}
return captchaResp.Success, nil return captchaResp.Success, nil
} }

View File

@ -1,7 +1,6 @@
appname = casdoor appname = casdoor
httpport = 8000 httpport = 8000
runmode = dev runmode = dev
SessionOn = true
copyrequestbody = true copyrequestbody = true
driverName = mysql driverName = mysql
dataSourceName = root:123456@tcp(localhost:3306)/ dataSourceName = root:123456@tcp(localhost:3306)/
@ -12,7 +11,7 @@ redisEndpoint =
defaultStorageProvider = defaultStorageProvider =
isCloudIntranet = false isCloudIntranet = false
authState = "casdoor" authState = "casdoor"
sock5Proxy = "127.0.0.1:10808" socks5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10 verificationCodeTimeout = 10
initScore = 2000 initScore = 2000
logPostOnly = true logPostOnly = true

View File

@ -79,13 +79,9 @@ func TestGetConfBool(t *testing.T) {
input string input string
expected interface{} expected interface{}
}{ }{
{"Should be return false", "SessionOn", false},
{"Should be return false", "copyrequestbody", true}, {"Should be return false", "copyrequestbody", true},
} }
//do some set up job
os.Setenv("SessionOn", "false")
err := beego.LoadAppConfig("ini", "app.conf") err := beego.LoadAppConfig("ini", "app.conf")
assert.Nil(t, err) assert.Nil(t, err)
for _, scenery := range scenarios { for _, scenery := range scenarios {

View File

@ -83,6 +83,9 @@ type Captcha struct {
CaptchaImage []byte `json:"captchaImage"` CaptchaImage []byte `json:"captchaImage"`
ClientId string `json:"clientId"` ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"` ClientSecret string `json:"clientSecret"`
ClientId2 string `json:"clientId2"`
ClientSecret2 string `json:"clientSecret2"`
SubType string `json:"subType"`
} }
// Signup // Signup
@ -214,7 +217,7 @@ func (c *ApiController) Signup() {
record.User = user.Name record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) }) util.SafeGoroutine(func() { object.AddRecord(record) })
userId := fmt.Sprintf("%s/%s", user.Owner, user.Name) userId := user.GetId()
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId) util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
c.ResponseOk(userId) c.ResponseOk(userId)
@ -225,7 +228,7 @@ func (c *ApiController) Signup() {
// @Tag Login API // @Tag Login API
// @Description logout the current user // @Description logout the current user
// @Success 200 {object} controllers.Response The Response object // @Success 200 {object} controllers.Response The Response object
// @router /logout [post] // @router /logout [get,post]
func (c *ApiController) Logout() { func (c *ApiController) Logout() {
user := c.GetSessionUsername() user := c.GetSessionUsername()
util.LogInfo(c.Ctx, "API: [%s] logged out", user) util.LogInfo(c.Ctx, "API: [%s] logged out", user)
@ -313,7 +316,14 @@ func (c *ApiController) GetCaptcha() {
c.ResponseOk(Captcha{Type: captchaProvider.Type, CaptchaId: id, CaptchaImage: img}) c.ResponseOk(Captcha{Type: captchaProvider.Type, CaptchaId: id, CaptchaImage: img})
return return
} else if captchaProvider.Type != "" { } else if captchaProvider.Type != "" {
c.ResponseOk(Captcha{Type: captchaProvider.Type, ClientId: captchaProvider.ClientId, ClientSecret: captchaProvider.ClientSecret}) c.ResponseOk(Captcha{
Type: captchaProvider.Type,
SubType: captchaProvider.SubType,
ClientId: captchaProvider.ClientId,
ClientSecret: captchaProvider.ClientSecret,
ClientId2: captchaProvider.ClientId2,
ClientSecret2: captchaProvider.ClientSecret2,
})
return return
} }
} }

View File

@ -50,6 +50,17 @@ func tokenToResponse(token *object.Token) *Response {
// HandleLoggedIn ... // HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) { func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
userId := user.GetId() userId := user.GetId()
allowed, err := object.CheckAccessPermission(userId, application)
if err != nil {
c.ResponseError(err.Error(), nil)
return
}
if !allowed {
c.ResponseError("Unauthorized operation")
return
}
if form.Type == ResponseTypeLogin { if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId) c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId) util.LogInfo(c.Ctx, "API: [%s] signed in", userId)

View File

@ -22,6 +22,7 @@ import (
type LinkForm struct { type LinkForm struct {
ProviderType string `json:"providerType"` ProviderType string `json:"providerType"`
User object.User `json:"user"`
} }
// Unlink ... // Unlink ...
@ -40,16 +41,55 @@ func (c *ApiController) Unlink() {
} }
providerType := form.ProviderType providerType := form.ProviderType
// the user will be unlinked from the provider
unlinkedUser := form.User
user := object.GetUser(userId) user := object.GetUser(userId)
value := object.GetUserField(user, providerType)
if user.Id != unlinkedUser.Id && !user.IsGlobalAdmin {
// if the user is not the same as the one we are unlinking, we need to make sure the user is the global admin.
c.ResponseError("You are not the global admin, you can't unlink other users")
return
}
if user.Id == unlinkedUser.Id && !user.IsGlobalAdmin {
// if the user is unlinking themselves, should check the provider can be unlinked, if not, we should return an error.
application := object.GetApplicationByUser(user)
if application == nil {
c.ResponseError("You can't unlink yourself, you are not a member of any application")
return
}
if len(application.Providers) == 0 {
c.ResponseError("This application has no providers")
return
}
provider := application.GetProviderItemByType(providerType)
if provider == nil {
c.ResponseError("This application has no providers of type " + providerType)
return
}
if !provider.CanUnlink {
c.ResponseError("This provider can't be unlinked")
return
}
}
// only two situations can happen here
// 1. the user is the global admin
// 2. the user is unlinking themselves and provider can be unlinked
value := object.GetUserField(&unlinkedUser, providerType)
if value == "" { if value == "" {
c.ResponseError("Please link first", value) c.ResponseError("Please link first", value)
return return
} }
object.ClearUserOAuthProperties(user, providerType) object.ClearUserOAuthProperties(&unlinkedUser, providerType)
object.LinkUserAccount(user, providerType, "") object.LinkUserAccount(&unlinkedUser, providerType, "")
c.ResponseOk() c.ResponseOk()
} }

View File

@ -30,6 +30,7 @@ type EmailForm struct {
Content string `json:"content"` Content string `json:"content"`
Sender string `json:"sender"` Sender string `json:"sender"`
Receivers []string `json:"receivers"` Receivers []string `json:"receivers"`
Provider string `json:"provider"`
} }
type SmsForm struct { type SmsForm struct {
@ -48,11 +49,6 @@ type SmsForm struct {
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /api/send-email [post] // @router /api/send-email [post]
func (c *ApiController) SendEmail() { func (c *ApiController) SendEmail() {
provider, _, ok := c.GetProviderFromContext("Email")
if !ok {
return
}
var emailForm EmailForm var emailForm EmailForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm) err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm)
@ -61,6 +57,29 @@ func (c *ApiController) SendEmail() {
return return
} }
var provider *object.Provider
if emailForm.Provider != "" {
// called by frontend's TestEmailWidget, provider name is set by frontend
provider = object.GetProvider(fmt.Sprintf("admin/%s", emailForm.Provider))
} else {
// called by Casdoor SDK via Client ID & Client Secret, so the used Email provider will be the application' Email provider or the default Email provider
var ok bool
provider, _, ok = c.GetProviderFromContext("Email")
if !ok {
return
}
}
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
err := object.DailSmtpServer(provider)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk()
}
if util.IsStrsEmpty(emailForm.Title, emailForm.Content, emailForm.Sender) { if util.IsStrsEmpty(emailForm.Title, emailForm.Content, emailForm.Sender) {
c.ResponseError(fmt.Sprintf("Empty parameters for emailForm: %v", emailForm)) c.ResponseError(fmt.Sprintf("Empty parameters for emailForm: %v", emailForm))
return return

View File

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

View File

@ -81,18 +81,26 @@ func (c *ApiController) GetUsers() {
// @Tag User API // @Tag User API
// @Description get user // @Description get user
// @Param id query string true "The id of the user" // @Param id query string true "The id of the user"
// @Param owner query string false "The owner of the user"
// @Param email query string false "The email of the user"
// @Param phone query string false "The phone of the user"
// @Success 200 {object} object.User The Response object // @Success 200 {object} object.User The Response object
// @router /get-user [get] // @router /get-user [get]
func (c *ApiController) GetUser() { func (c *ApiController) GetUser() {
id := c.Input().Get("id") id := c.Input().Get("id")
owner := c.Input().Get("owner")
email := c.Input().Get("email") email := c.Input().Get("email")
userOwner, _ := util.GetOwnerAndNameFromId(id) phone := c.Input().Get("phone")
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", userOwner)) userId := c.Input().Get("userId")
owner := c.Input().Get("owner")
if owner == "" {
owner, _ = util.GetOwnerAndNameFromId(id)
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner))
if !organization.IsProfilePublic { if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, false) hasPermission, err := object.CheckUserPermission(requestUserId, id, owner, false)
if !hasPermission { if !hasPermission {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -100,10 +108,22 @@ func (c *ApiController) GetUser() {
} }
var user *object.User var user *object.User
if email == "" { switch {
user = object.GetUser(id) case email != "":
} else {
user = object.GetUserByEmail(owner, email) user = object.GetUserByEmail(owner, email)
case phone != "":
user = object.GetUserByPhone(owner, phone)
case userId != "":
user = object.GetUserByUserId(owner, userId)
default:
user = object.GetUser(id)
}
if user != nil {
roles := object.GetRolesByUser(user.GetId())
user.Roles = roles
permissions := object.GetPermissionsByUser(user.GetId())
user.Permissions = permissions
} }
c.Data["json"] = object.GetMaskedUser(user) c.Data["json"] = object.GetMaskedUser(user)
@ -246,7 +266,7 @@ func (c *ApiController) SetPassword() {
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
userId := fmt.Sprintf("%s/%s", userOwner, userName) userId := fmt.Sprintf("%s/%s", userOwner, userName)
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true) hasPermission, err := object.CheckUserPermission(requestUserId, userId, userOwner, true)
if !hasPermission { if !hasPermission {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -51,6 +51,23 @@ func (c *ApiController) ResponseError(error string, data ...interface{}) {
c.ServeJSON() 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 ... // RequireSignedIn ...
func (c *ApiController) RequireSignedIn() (string, bool) { func (c *ApiController) RequireSignedIn() (string, bool) {
userId := c.GetSessionUsername() userId := c.GetSessionUsername()

View File

@ -42,15 +42,31 @@ func (c *ApiController) getCurrentUser() *object.User {
func (c *ApiController) SendVerificationCode() { func (c *ApiController) SendVerificationCode() {
destType := c.Ctx.Request.Form.Get("type") destType := c.Ctx.Request.Form.Get("type")
dest := c.Ctx.Request.Form.Get("dest") dest := c.Ctx.Request.Form.Get("dest")
orgId := c.Ctx.Request.Form.Get("organizationId")
checkType := c.Ctx.Request.Form.Get("checkType") checkType := c.Ctx.Request.Form.Get("checkType")
checkId := c.Ctx.Request.Form.Get("checkId") checkId := c.Ctx.Request.Form.Get("checkId")
checkKey := c.Ctx.Request.Form.Get("checkKey") checkKey := c.Ctx.Request.Form.Get("checkKey")
checkUser := c.Ctx.Request.Form.Get("checkUser") checkUser := c.Ctx.Request.Form.Get("checkUser")
applicationId := c.Ctx.Request.Form.Get("applicationId")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request) remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if len(destType) == 0 || len(dest) == 0 || len(orgId) == 0 || !strings.Contains(orgId, "/") || len(checkType) == 0 { if destType == "" {
c.ResponseError("Missing parameter.") c.ResponseError("Missing parameter: type.")
return
}
if dest == "" {
c.ResponseError("Missing parameter: dest.")
return
}
if applicationId == "" {
c.ResponseError("Missing parameter: applicationId.")
return
}
if !strings.Contains(applicationId, "/") {
c.ResponseError("Wrong parameter: applicationId.")
return
}
if checkType == "" {
c.ResponseError("Missing parameter: checkType.")
return return
} }
@ -63,7 +79,7 @@ func (c *ApiController) SendVerificationCode() {
} }
isHuman, err := captchaProvider.VerifyCaptcha(checkKey, checkId) isHuman, err := captchaProvider.VerifyCaptcha(checkKey, checkId)
if err != nil { if err != nil {
c.ResponseError("Failed to verify captcha: %v", err) c.ResponseError(err.Error())
return return
} }
@ -74,8 +90,8 @@ func (c *ApiController) SendVerificationCode() {
} }
user := c.getCurrentUser() user := c.getCurrentUser()
organization := object.GetOrganization(orgId) application := object.GetApplication(applicationId)
application := object.GetApplicationByOrganizationName(organization.Name) organization := object.GetOrganization(fmt.Sprintf("%s/%s", application.Owner, application.Organization))
if checkUser == "true" && user == nil && object.GetUserByFields(organization.Name, dest) == nil { if checkUser == "true" && user == nil && object.GetUserByFields(organization.Name, dest) == nil {
c.ResponseError("Please login first") c.ResponseError("Please login first")
@ -85,7 +101,7 @@ func (c *ApiController) SendVerificationCode() {
sendResp := errors.New("Invalid dest type") sendResp := errors.New("Invalid dest type")
if user == nil && checkUser != "" && checkUser != "true" { if user == nil && checkUser != "" && checkUser != "true" {
_, name := util.GetOwnerAndNameFromId(orgId) name := application.Organization
user = object.GetUser(fmt.Sprintf("%s/%s", name, checkUser)) user = object.GetUser(fmt.Sprintf("%s/%s", name, checkUser))
} }
switch destType { switch destType {
@ -108,13 +124,12 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError("Invalid phone number") c.ResponseError("Invalid phone number")
return return
} }
org := object.GetOrganization(orgId) if organization == nil {
if org == nil { c.ResponseError("The organization doesn't exist.")
c.ResponseError("Missing parameter.")
return return
} }
dest = fmt.Sprintf("+%s%s", org.PhonePrefix, dest) dest = fmt.Sprintf("+%s%s", organization.PhonePrefix, dest)
provider := application.GetSmsProvider() provider := application.GetSmsProvider()
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, dest) sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, dest)
} }
@ -153,13 +168,35 @@ func (c *ApiController) ResetEmailOrPhone() {
} }
checkDest := dest checkDest := dest
if destType == "phone" {
org := object.GetOrganizationByUser(user) org := object.GetOrganizationByUser(user)
if destType == "phone" {
phoneItem := object.GetAccountItemByName("Phone", org)
if phoneItem == nil {
c.ResponseError("Unable to get the phone modify rule.")
return
}
if pass, errMsg := object.CheckAccountItemModifyRule(phoneItem, user); !pass {
c.ResponseError(errMsg)
return
}
phonePrefix := "86" phonePrefix := "86"
if org != nil && org.PhonePrefix != "" { if org != nil && org.PhonePrefix != "" {
phonePrefix = org.PhonePrefix phonePrefix = org.PhonePrefix
} }
checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest) checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest)
} else if destType == "email" {
emailItem := object.GetAccountItemByName("Email", org)
if emailItem == nil {
c.ResponseError("Unable to get the email modify rule.")
return
}
if pass, errMsg := object.CheckAccountItemModifyRule(emailItem, user); !pass {
c.ResponseError(errMsg)
return
}
} }
if ret := object.CheckVerificationCode(checkDest, code); len(ret) != 0 { if ret := object.CheckVerificationCode(checkDest, code); len(ret) != 0 {
c.ResponseError(ret) c.ResponseError(ret)
@ -209,7 +246,7 @@ func (c *ApiController) VerifyCaptcha() {
isValid, err := provider.VerifyCaptcha(captchaToken, clientSecret) isValid, err := provider.VerifyCaptcha(captchaToken, clientSecret)
if err != nil { if err != nil {
c.ResponseError("Failed to verify captcha: %v", err) c.ResponseError(err.Error())
return return
} }

138
controllers/webauthn.go Normal file
View File

@ -0,0 +1,138 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"bytes"
"io/ioutil"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
// @Title WebAuthnSignupBegin
// @Tag User API
// @Description WebAuthn Registration Flow 1st stage
// @Success 200 {object} protocol.CredentialCreation The CredentialCreationOptions object
// @router /webauthn/signup/begin [get]
func (c *ApiController) WebAuthnSignupBegin() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
user := c.getCurrentUser()
if user == nil {
c.ResponseError("Please login first.")
return
}
registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
}
options, sessionData, err := webauthnObj.BeginRegistration(
user,
registerOptions,
)
if err != nil {
c.ResponseError(err.Error())
return
}
c.SetSession("registration", *sessionData)
c.Data["json"] = options
c.ServeJSON()
}
// @Title WebAuthnSignupFinish
// @Tag User API
// @Description WebAuthn Registration Flow 2nd stage
// @Param body body protocol.CredentialCreationResponse true "authenticator attestation Response"
// @Success 200 {object} Response "The Response object"
// @router /webauthn/signup/finish [post]
func (c *ApiController) WebAuthnSignupFinish() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
user := c.getCurrentUser()
if user == nil {
c.ResponseError("Please login first.")
return
}
sessionObj := c.GetSession("registration")
sessionData, ok := sessionObj.(webauthn.SessionData)
if !ok {
c.ResponseError("Please call WebAuthnSignupBegin first")
return
}
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
credential, err := webauthnObj.FinishRegistration(user, sessionData, c.Ctx.Request)
if err != nil {
c.ResponseError(err.Error())
return
}
isGlobalAdmin := c.IsGlobalAdmin()
user.AddCredentials(*credential, isGlobalAdmin)
c.ResponseOk()
}
// @Title WebAuthnSigninBegin
// @Tag Login API
// @Description WebAuthn Login Flow 1st stage
// @Param owner query string true "owner"
// @Param name query string true "name"
// @Success 200 {object} protocol.CredentialAssertion The CredentialAssertion object
// @router /webauthn/signin/begin [get]
func (c *ApiController) WebAuthnSigninBegin() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
userOwner := c.Input().Get("owner")
userName := c.Input().Get("name")
user := object.GetUserByFields(userOwner, userName)
if user == nil {
c.ResponseError("Please Giveout Owner and Username.")
return
}
options, sessionData, err := webauthnObj.BeginLogin(user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.SetSession("authentication", *sessionData)
c.Data["json"] = options
c.ServeJSON()
}
// @Title WebAuthnSigninBegin
// @Tag Login API
// @Description WebAuthn Login Flow 2nd stage
// @Param body body protocol.CredentialAssertionResponse true "authenticator assertion Response"
// @Success 200 {object} Response "The Response object"
// @router /webauthn/signin/finish [post]
func (c *ApiController) WebAuthnSigninFinish() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
sessionObj := c.GetSession("authentication")
sessionData, ok := sessionObj.(webauthn.SessionData)
if !ok {
c.ResponseError("Please call WebAuthnSigninBegin first")
return
}
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
userId := string(sessionData.UserID)
user := object.GetUser(userId)
_, err := webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
if err != nil {
c.ResponseError(err.Error())
return
}
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
c.ResponseOk(userId)
}

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ services:
build: build:
context: ./ context: ./
dockerfile: Dockerfile dockerfile: Dockerfile
target: STANDARD
entrypoint: /bin/sh -c './server --createDatabase=true' entrypoint: /bin/sh -c './server --createDatabase=true'
ports: ports:
- "8000:8000" - "8000:8000"

8
docker-entrypoint.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ;fi
service mariadb start
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
exec /server --createDatabase=true

3
go.mod
View File

@ -11,9 +11,10 @@ require (
github.com/casbin/casbin/v2 v2.30.1 github.com/casbin/casbin/v2 v2.30.1
github.com/casbin/xorm-adapter/v2 v2.5.1 github.com/casbin/xorm-adapter/v2 v2.5.1
github.com/casdoor/go-sms-sender v0.2.0 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/casdoor/oss v1.2.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-pay/gopay v1.5.72 github.com/go-pay/gopay v1.5.72

18
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/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 h1:52bin4EBOPzOee64s9UK7jxd22FODvT9/+Y/Z+PSHpg=
github.com/casdoor/go-sms-sender v0.2.0/go.mod h1:fsZsNnALvFIo+HFcE1U/oCQv4ZT42FdglXKMsEm3WSk= 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-FIX2 h1:RgfIMkL9kekylgxHHK2ZY8ASAwOGns2HVlaBwLu7Bcs=
github.com/casdoor/goth v1.69.0-FIX1/go.mod h1:Om55nRo8CkeDkPSNBbzXW4G5uI28ZUkSk5S69dPek3s= 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 h1:ozLAE+nnNdFQBWbzH8U9spzaO8h8NrB57lBcdyMUUQ8=
github.com/casdoor/oss v1.2.0/go.mod h1:qii35VBuxnR/uEuYSKpS0aJ8htQFOcCVsZ4FHgHLuss= 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= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -111,6 +111,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1CUzYdAZXijuvLuRMirgiXdf3zsM2Ig=
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
@ -124,6 +126,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M= github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY= github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b h1:L63RATZFZuFMXy6ixnKmv3eNAXwYQF6HW1vd4IYsQqQ=
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b/go.mod h1:EYSpSkwoEcryMmQGfhol2IiB3IMN9IIIaNd/wcAQMGQ=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
@ -135,6 +139,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ=
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
@ -164,6 +170,7 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@ -201,6 +208,8 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -298,6 +307,8 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -397,6 +408,8 @@ github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnD
github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA= github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA=
github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4= github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -413,6 +426,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

View File

@ -39,6 +39,7 @@ func readI18nFile(language string) *I18nData {
func writeI18nFile(language string, data *I18nData) { func writeI18nFile(language string, data *I18nData) {
s := util.StructToJsonFormatted(data) s := util.StructToJsonFormatted(data)
s = strings.ReplaceAll(s, "\\u0026", "&") s = strings.ReplaceAll(s, "\\u0026", "&")
s += "\n"
println(s) println(s)
util.WriteStringToPath(s, getI18nFilePath(language)) util.WriteStringToPath(s, getI18nFilePath(language))

View File

@ -144,7 +144,7 @@ func (idp *BilibiliIdProvider) GetToken(code string) (*oauth2.Token, error) {
type BilibiliUserInfo struct { type BilibiliUserInfo struct {
Name string `json:"name"` Name string `json:"name"`
Face string `json:"face"` Face string `json:"face"`
OpenId string `json:"openid` OpenId string `json:"openid"`
} }
type BilibiliUserInfoResponse struct { type BilibiliUserInfoResponse struct {

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,
"certificate": "",
"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.InitAdapter(*createDatabase)
object.InitDb() object.InitDb()
object.InitFromFile()
object.InitDefaultStorageProvider() object.InitDefaultStorageProvider()
object.InitLdapAutoSynchronizer() object.InitLdapAutoSynchronizer()
proxy.InitHttpClient() proxy.InitHttpClient()
@ -51,9 +52,11 @@ func main() {
// https://studygolang.com/articles/2303 // https://studygolang.com/articles/2303
beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AuthzFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.AuthzFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage) beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
beego.BConfig.WebConfig.Session.SessionOn = true
beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id" beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id"
if conf.GetConfigString("redisEndpoint") == "" { if conf.GetConfigString("redisEndpoint") == "" {
beego.BConfig.WebConfig.Session.SessionProvider = "file" beego.BConfig.WebConfig.Session.SessionProvider = "file"

View File

@ -16,7 +16,7 @@ data:
defaultStorageProvider = defaultStorageProvider =
isCloudIntranet = false isCloudIntranet = false
authState = "casdoor" authState = "casdoor"
sock5Proxy = "127.0.0.1:10808" socks5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10 verificationCodeTimeout = 10
initScore = 2000 initScore = 2000
logPostOnly = true logPostOnly = true

View File

@ -24,6 +24,7 @@ import (
//_ "github.com/denisenkom/go-mssqldb" // db = mssql //_ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql _ "github.com/go-sql-driver/mysql" // db = mysql
//_ "github.com/lib/pq" // db = postgres //_ "github.com/lib/pq" // db = postgres
//_ "github.com/mattn/go-sqlite3" // db = sqlite3
"xorm.io/core" "xorm.io/core"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -36,11 +37,12 @@ func InitConfig() {
panic(err) panic(err)
} }
beego.BConfig.WebConfig.Session.SessionOn = true
InitAdapter(true) InitAdapter(true)
} }
func InitAdapter(createDatabase bool) { func InitAdapter(createDatabase bool) {
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName(), conf.GetConfigString("dbName")) adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName(), conf.GetConfigString("dbName"))
if createDatabase { if createDatabase {
adapter.CreateDatabase() adapter.CreateDatabase()
@ -202,6 +204,11 @@ func (a *Adapter) createTable() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(PermissionRule))
if err != nil {
panic(err)
}
} }
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session { func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {

View File

@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"net/url"
"strings" "strings"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -45,6 +46,8 @@ type Application struct {
EnableSignUp bool `json:"enableSignUp"` EnableSignUp bool `json:"enableSignUp"`
EnableSigninSession bool `json:"enableSigninSession"` EnableSigninSession bool `json:"enableSigninSession"`
EnableCodeSignin bool `json:"enableCodeSignin"` EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"`
EnableWebAuthn bool `json:"enableWebAuthn"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
@ -278,8 +281,12 @@ func UpdateApplication(id string, application *Application) bool {
} }
func AddApplication(application *Application) bool { func AddApplication(application *Application) bool {
if application.ClientId == "" {
application.ClientId = util.GenerateClientId() application.ClientId = util.GenerateClientId()
}
if application.ClientSecret == "" {
application.ClientSecret = util.GenerateClientSecret() application.ClientSecret = util.GenerateClientSecret()
}
for _, providerItem := range application.Providers { for _, providerItem := range application.Providers {
providerItem.Provider = nil providerItem.Provider = nil
} }
@ -319,3 +326,39 @@ func CheckRedirectUriValid(application *Application, redirectUri string) bool {
} }
return validUri 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 { func getPermanentAvatarUrl(organization string, username string, url string) string {
if url == "" {
return ""
}
if defaultStorageProvider == nil { if defaultStorageProvider == nil {
return "" return ""
} }

View File

@ -33,7 +33,7 @@ type Cert struct {
BitSize int `json:"bitSize"` BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"` ExpireInYears int `json:"expireInYears"`
PublicKey string `xorm:"mediumtext" json:"publicKey"` Certificate string `xorm:"mediumtext" json:"certificate"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"` PrivateKey string `xorm:"mediumtext" json:"privateKey"`
AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"` AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"`
AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"` AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"`
@ -123,9 +123,9 @@ func UpdateCert(id string, cert *Cert) bool {
} }
func AddCert(cert *Cert) bool { func AddCert(cert *Cert) bool {
if cert.PublicKey == "" || cert.PrivateKey == "" { if cert.Certificate == "" || cert.PrivateKey == "" {
publicKey, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner) certificate, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
cert.PublicKey = publicKey cert.Certificate = certificate
cert.PrivateKey = privateKey cert.PrivateKey = privateKey
} }

View File

@ -197,16 +197,20 @@ func filterField(field string) bool {
return reFieldWhiteList.MatchString(field) return reFieldWhiteList.MatchString(field)
} }
func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error) { func CheckUserPermission(requestUserId, userId, userOwner string, strict bool) (bool, error) {
if requestUserId == "" { if requestUserId == "" {
return false, fmt.Errorf("please login first") return false, fmt.Errorf("please login first")
} }
if userId != "" {
targetUser := GetUser(userId) targetUser := GetUser(userId)
if targetUser == nil { if targetUser == nil {
return false, fmt.Errorf("the user: %s doesn't exist", userId) return false, fmt.Errorf("the user: %s doesn't exist", userId)
} }
userOwner = targetUser.Owner
}
hasPermission := false hasPermission := false
if strings.HasPrefix(requestUserId, "app/") { if strings.HasPrefix(requestUserId, "app/") {
hasPermission = true hasPermission = true
@ -219,7 +223,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error
hasPermission = true hasPermission = true
} else if requestUserId == userId { } else if requestUserId == userId {
hasPermission = true hasPermission = true
} else if targetUser.Owner == requestUser.Owner { } else if userOwner == requestUser.Owner {
if strict { if strict {
hasPermission = requestUser.IsAdmin hasPermission = requestUser.IsAdmin
} else { } else {
@ -230,3 +234,29 @@ func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error
return hasPermission, fmt.Errorf("you don't have the permission to do this") return hasPermission, fmt.Errorf("you don't have the permission to do this")
} }
func CheckAccessPermission(userId string, application *Application) (bool, error) {
permissions := GetPermissions(application.Organization)
allowed := true
var err error
for _, permission := range permissions {
if !permission.IsEnabled || len(permission.Users) == 0 {
continue
}
isHit := false
for _, resource := range permission.Resources {
if application.Name == resource {
isHit = true
break
}
}
if isHit {
enforcer := getEnforcer(permission)
allowed, err = enforcer.Enforce(userId, application.Name, "read")
break
}
}
return allowed, err
}

View File

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

View File

@ -15,19 +15,25 @@
package object package object
import ( import (
"encoding/gob"
"io/ioutil" "io/ioutil"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn"
) )
func InitDb() { func InitDb() {
existed := initBuiltInOrganization() existed := initBuiltInOrganization()
if !existed { if !existed {
initBuiltInPermission()
initBuiltInProvider()
initBuiltInUser() initBuiltInUser()
initBuiltInApplication() initBuiltInApplication()
initBuiltInCert() initBuiltInCert()
initBuiltInLdap() initBuiltInLdap()
} }
initWebAuthn()
} }
func initBuiltInOrganization() bool { func initBuiltInOrganization() bool {
@ -65,12 +71,15 @@ func initBuiltInOrganization() bool {
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"}, {Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, 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 global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
}, },
} }
AddOrganization(organization) AddOrganization(organization)
@ -103,7 +112,7 @@ func initBuiltInUser() {
IsGlobalAdmin: true, IsGlobalAdmin: true,
IsForbidden: false, IsForbidden: false,
IsDeleted: false, IsDeleted: false,
SignupApplication: "built-in-app", SignupApplication: "app-built-in",
CreatedIp: "127.0.0.1", CreatedIp: "127.0.0.1",
Properties: make(map[string]string), Properties: make(map[string]string),
} }
@ -127,7 +136,9 @@ func initBuiltInApplication() {
Cert: "cert-built-in", Cert: "cert-built-in",
EnablePassword: true, EnablePassword: true,
EnableSignUp: true, EnableSignUp: true,
Providers: []*ProviderItem{}, Providers: []*ProviderItem{
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Provider: nil},
},
SignupItems: []*SignupItem{ SignupItems: []*SignupItem{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"}, {Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},
{Name: "Username", Visible: true, Required: true, Prompted: false, Rule: "None"}, {Name: "Username", Visible: true, Required: true, Prompted: false, Rule: "None"},
@ -159,7 +170,7 @@ func readTokenFromFile() (string, string) {
} }
func initBuiltInCert() { func initBuiltInCert() {
tokenJwtPublicKey, tokenJwtPrivateKey := readTokenFromFile() tokenJwtCertificate, tokenJwtPrivateKey := readTokenFromFile()
cert := getCert("admin", "cert-built-in") cert := getCert("admin", "cert-built-in")
if cert != nil { if cert != nil {
return return
@ -175,7 +186,7 @@ func initBuiltInCert() {
CryptoAlgorithm: "RS256", CryptoAlgorithm: "RS256",
BitSize: 4096, BitSize: 4096,
ExpireInYears: 20, ExpireInYears: 20,
PublicKey: tokenJwtPublicKey, Certificate: tokenJwtCertificate,
PrivateKey: tokenJwtPrivateKey, PrivateKey: tokenJwtPrivateKey,
} }
AddCert(cert) AddCert(cert)
@ -201,3 +212,46 @@ func initBuiltInLdap() {
} }
AddLdap(ldap) AddLdap(ldap)
} }
func initBuiltInProvider() {
provider := GetProvider("admin/provider_captcha_default")
if provider != nil {
return
}
provider = &Provider{
Owner: "admin",
Name: "provider_captcha_default",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Captcha Default",
Category: "Captcha",
Type: "Default",
}
AddProvider(provider)
}
func initWebAuthn() {
gob.Register(webauthn.SessionData{})
}
func initBuiltInPermission() {
permission := GetPermission("built-in/permission-built-in")
if permission != nil {
return
}
permission = &Permission{
Owner: "built-in",
Name: "permission-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Permission",
Users: []string{"built-in/admin"},
Roles: []string{},
ResourceType: "Application",
Resources: []string{"app-built-in"},
Actions: []string{"Read", "Write", "Admin"},
Effect: "Allow",
IsEnabled: true,
}
AddPermission(permission)
}

148
object/init_data.go Normal file
View File

@ -0,0 +1,148 @@
// 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: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{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

@ -97,7 +97,7 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
//link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html //link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
//or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key //or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
for _, cert := range certs { for _, cert := range certs {
certPemBlock := []byte(cert.PublicKey) certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock) certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes) x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)

View File

@ -15,6 +15,8 @@
package object package object
import ( import (
"fmt"
"github.com/casdoor/casdoor/cred" "github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"xorm.io/core" "xorm.io/core"
@ -186,3 +188,31 @@ func DeleteOrganization(organization *Organization) bool {
func GetOrganizationByUser(user *User) *Organization { func GetOrganizationByUser(user *User) *Organization {
return getOrganization("admin", user.Owner) return getOrganization("admin", user.Owner)
} }
func GetAccountItemByName(name string, organization *Organization) *AccountItem {
if organization == nil {
return nil
}
for _, accountItem := range organization.AccountItems {
if accountItem.Name == name {
return accountItem
}
}
return nil
}
func CheckAccountItemModifyRule(accountItem *AccountItem, user *User) (bool, string) {
switch accountItem.ModifyRule {
case "Admin":
if !(user.IsAdmin || user.IsGlobalAdmin) {
return false, fmt.Sprintf("Only admin can modify the %s.", accountItem.Name)
}
case "Immutable":
return false, fmt.Sprintf("The %s is immutable.", accountItem.Name)
case "Self":
break
default:
return false, fmt.Sprintf("Unknown modify rule %s.", accountItem.ModifyRule)
}
return true, ""
}

View File

@ -16,7 +16,12 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
xormadapter "github.com/casbin/xorm-adapter/v2"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"xorm.io/core" "xorm.io/core"
) )
@ -39,6 +44,16 @@ type Permission struct {
IsEnabled bool `json:"isEnabled"` IsEnabled bool `json:"isEnabled"`
} }
type PermissionRule struct {
PType string `xorm:"varchar(100) index not null default ''"`
V0 string `xorm:"varchar(100) index not null default ''"`
V1 string `xorm:"varchar(100) index not null default ''"`
V2 string `xorm:"varchar(100) index not null default ''"`
V3 string `xorm:"varchar(100) index not null default ''"`
V4 string `xorm:"varchar(100) index not null default ''"`
V5 string `xorm:"varchar(100) index not null default ''"`
}
func GetPermissionCount(owner, field, value string) int { func GetPermissionCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Permission{}) count, err := session.Count(&Permission{})
@ -95,7 +110,8 @@ func GetPermission(id string) *Permission {
func UpdatePermission(id string, permission *Permission) bool { func UpdatePermission(id string, permission *Permission) bool {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if getPermission(owner, name) == nil { oldPermission := getPermission(owner, name)
if oldPermission == nil {
return false return false
} }
@ -104,6 +120,11 @@ func UpdatePermission(id string, permission *Permission) bool {
panic(err) panic(err)
} }
if affected != 0 {
removePolicies(oldPermission)
addPolicies(permission)
}
return affected != 0 return affected != 0
} }
@ -113,6 +134,10 @@ func AddPermission(permission *Permission) bool {
panic(err) panic(err)
} }
if affected != 0 {
addPolicies(permission)
}
return affected != 0 return affected != 0
} }
@ -122,9 +147,95 @@ func DeletePermission(permission *Permission) bool {
panic(err) panic(err)
} }
if affected != 0 {
removePolicies(permission)
}
return affected != 0 return affected != 0
} }
func (permission *Permission) GetId() string { func (permission *Permission) GetId() string {
return fmt.Sprintf("%s/%s", permission.Owner, permission.Name) return fmt.Sprintf("%s/%s", permission.Owner, permission.Name)
} }
func getEnforcer(permission *Permission) *casbin.Enforcer {
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), "permission_rule", tableNamePrefix, true)
if err != nil {
panic(err)
}
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = permission, sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act`
permissionModel := getModel(permission.Owner, permission.Model)
if permissionModel != nil {
modelText = permissionModel.ModelText
}
m, err := model.NewModelFromString(modelText)
if err != nil {
panic(err)
}
enforcer, err := casbin.NewEnforcer(m, adapter)
if err != nil {
panic(err)
}
err = enforcer.LoadFilteredPolicy(xormadapter.Filter{V0: []string{permission.GetId()}})
if err != nil {
panic(err)
}
return enforcer
}
func getPolicies(permission *Permission) [][]string {
var policies [][]string
for _, user := range permission.Users {
for _, resource := range permission.Resources {
for _, action := range permission.Actions {
policies = append(policies, []string{permission.GetId(), user, resource, strings.ToLower(action)})
}
}
}
return policies
}
func addPolicies(permission *Permission) {
enforcer := getEnforcer(permission)
policies := getPolicies(permission)
_, err := enforcer.AddPolicies(policies)
if err != nil {
panic(err)
}
}
func removePolicies(permission *Permission) {
enforcer := getEnforcer(permission)
_, err := enforcer.RemoveFilteredPolicy(0, permission.GetId())
if err != nil {
panic(err)
}
}
func GetPermissionsByUser(userId string) []*Permission {
permissions := []*Permission{}
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&permissions)
if err != nil {
panic(err)
}
return permissions
}

View File

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

View File

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

View File

@ -33,6 +33,15 @@ func (application *Application) GetProviderItem(providerName string) *ProviderIt
return nil return nil
} }
func (application *Application) GetProviderItemByType(providerType string) *ProviderItem {
for _, item := range application.Providers {
if item.Provider.Type == providerType {
return item
}
}
return nil
}
func (pi *ProviderItem) IsProviderVisible() bool { func (pi *ProviderItem) IsProviderVisible() bool {
if pi.Provider == nil { if pi.Provider == nil {
return false return false

View File

@ -121,3 +121,13 @@ func DeleteRole(role *Role) bool {
func (role *Role) GetId() string { func (role *Role) GetId() string {
return fmt.Sprintf("%s/%s", role.Owner, role.Name) return fmt.Sprintf("%s/%s", role.Owner, role.Name)
} }
func GetRolesByUser(userId string) []*Role {
roles := []*Role{}
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&roles)
if err != nil {
panic(err)
}
return roles
}

View File

@ -36,7 +36,7 @@ import (
) )
//returns a saml2 response //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, certificate string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
samlResponse := &etree.Element{ samlResponse := &etree.Element{
Space: "samlp", Space: "samlp",
Tag: "Response", Tag: "Response",
@ -51,7 +51,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
samlResponse.CreateAttr("Version", "2.0") samlResponse.CreateAttr("Version", "2.0")
samlResponse.CreateAttr("IssueInstant", now) samlResponse.CreateAttr("IssueInstant", now)
samlResponse.CreateAttr("Destination", destination) samlResponse.CreateAttr("Destination", destination)
samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId)) samlResponse.CreateAttr("InResponseTo", requestId)
samlResponse.CreateElement("saml:Issuer").SetText(host) samlResponse.CreateElement("saml:Issuer").SetText(host)
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success") samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
@ -68,7 +68,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation") subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer") subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer")
subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData") subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData")
subjectConfirmationData.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId)) subjectConfirmationData.CreateAttr("InResponseTo", requestId)
subjectConfirmationData.CreateAttr("Recipient", destination) subjectConfirmationData.CreateAttr("Recipient", destination)
subjectConfirmationData.CreateAttr("NotOnOrAfter", expireTime) subjectConfirmationData.CreateAttr("NotOnOrAfter", expireTime)
condition := assertion.CreateElement("saml:Conditions") condition := assertion.CreateElement("saml:Conditions")
@ -177,8 +177,8 @@ type Attribute struct {
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
//_, originBackend := getOriginFromHost(host) //_, originBackend := getOriginFromHost(host)
cert := getCertByApplication(application) cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.PublicKey)) block, _ := pem.Decode([]byte(cert.Certificate))
publicKey := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
origin := beego.AppConfig.String("origin") origin := beego.AppConfig.String("origin")
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
@ -199,7 +199,7 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
KeyInfo: KeyInfo{ KeyInfo: KeyInfo{
X509Data: X509Data{ X509Data: X509Data{
X509Certificate: X509Certificate{ X509Certificate: X509Certificate{
Cert: publicKey, Cert: certificate,
}, },
}, },
}, },
@ -225,14 +225,15 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
return &d, nil return &d, nil
} }
//GenerateSamlResponse generates a SAML2.0 response // GetSamlResponse generates a SAML2.0 response
// parameter samlRequest is saml request in base64 format // parameter samlRequest is saml request in base64 format
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, error) { func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, error) {
//decode samlRequest // base64 decode
defated, err := base64.StdEncoding.DecodeString(samlRequest) defated, err := base64.StdEncoding.DecodeString(samlRequest)
if err != nil { if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error()) return "", "", fmt.Errorf("err: %s", err.Error())
} }
// decompress
var buffer bytes.Buffer var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated)) rdr := flate.NewReader(bytes.NewReader(defated))
io.Copy(&buffer, rdr) io.Copy(&buffer, rdr)
@ -241,23 +242,24 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
if err != nil { if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error()) return "", "", fmt.Errorf("err: %s", err.Error())
} }
// verify samlRequest // verify samlRequest
if valid := CheckRedirectUriValid(application, authnRequest.Issuer.Url); !valid { if valid := CheckRedirectUriValid(application, authnRequest.Issuer.Url); !valid {
return "", "", fmt.Errorf("err: invalid issuer url") return "", "", fmt.Errorf("err: invalid issuer url")
} }
//get publickey string // get certificate string
cert := getCertByApplication(application) cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.PublicKey)) block, _ := pem.Decode([]byte(cert.Certificate))
publicKey := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
_, originBackend := getOriginFromHost(host) _, originBackend := getOriginFromHost(host)
// build signedResponse // build signedResponse
samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, application.RedirectUris) samlResponse, _ := NewSamlResponse(user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
randomKeyStore := &X509Key{ randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey, PrivateKey: cert.PrivateKey,
X509Certificate: publicKey, X509Certificate: certificate,
} }
ctx := dsig.NewDefaultSigningContext(randomKeyStore) ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1 ctx.Hash = crypto.SHA1
@ -270,15 +272,28 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
doc := etree.NewDocument() doc := etree.NewDocument()
doc.SetRoot(samlResponse) doc.SetRoot(samlResponse)
xmlStr, err := doc.WriteToString() xmlBytes, err := doc.WriteToBytes()
if err != nil { if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error()) return "", "", fmt.Errorf("err: %s", err.Error())
} }
res := base64.StdEncoding.EncodeToString([]byte(xmlStr))
// 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()
}
// base64 encode
res := base64.StdEncoding.EncodeToString(xmlBytes)
return res, authnRequest.AssertionConsumerServiceURL, nil 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 { func NewSamlResponse11(user *User, requestID string, host string) *etree.Element {
samlResponse := &etree.Element{ samlResponse := &etree.Element{
Space: "samlp", Space: "samlp",

View File

@ -17,6 +17,7 @@ package object
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"net/url"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -42,8 +43,19 @@ func getProviderEndpoint(provider *Provider) string {
return endpoint return endpoint
} }
func escapePath(path string) string {
tokens := strings.Split(path, "/")
if len(tokens) > 0 {
tokens[len(tokens)-1] = url.QueryEscape(tokens[len(tokens)-1])
}
res := strings.Join(tokens, "/")
return res
}
func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) { func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), fullFilePath) escapedPath := escapePath(fullFilePath)
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
host := "" host := ""
if provider.Type != "Local File System" { if provider.Type != "Local File System" {
@ -60,9 +72,9 @@ func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
host = fmt.Sprintf("%s/%s", host, provider.Bucket) host = fmt.Sprintf("%s/%s", host, provider.Bucket)
} }
fileUrl := util.UrlJoin(host, objectKey) fileUrl := util.UrlJoin(host, escapePath(objectKey))
if hasTimestamp { if hasTimestamp {
fileUrl = fmt.Sprintf("%s?t=%s", util.UrlJoin(host, objectKey), util.GetCurrentUnixTime()) fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime())
} }
return fileUrl, objectKey return fileUrl, objectKey

View File

@ -22,7 +22,7 @@ import (
func (syncer *Syncer) syncUsers() { func (syncer *Syncer) syncUsers() {
fmt.Printf("Running syncUsers()..\n") fmt.Printf("Running syncUsers()..\n")
users, userMap := syncer.getUserMap() users, userMap, userNameMap := syncer.getUserMap()
oUsers, oUserMap, err := syncer.getOriginalUserMap() oUsers, oUserMap, err := syncer.getOriginalUserMap()
if err != nil { if err != nil {
fmt.Printf(err.Error()) fmt.Printf(err.Error())
@ -44,9 +44,11 @@ func (syncer *Syncer) syncUsers() {
for _, oUser := range oUsers { for _, oUser := range oUsers {
id := oUser.Id id := oUser.Id
if _, ok := userMap[id]; !ok { if _, ok := userMap[id]; !ok {
if _, ok := userNameMap[oUser.Name]; !ok {
newUser := syncer.createUserFromOriginalUser(oUser, affiliationMap) newUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
fmt.Printf("New user: %v\n", newUser) fmt.Printf("New user: %v\n", newUser)
newUsers = append(newUsers, newUser) newUsers = append(newUsers, newUser)
}
} else { } else {
user := userMap[id] user := userMap[id]
oHash := syncer.calculateHash(oUser) oHash := syncer.calculateHash(oUser)

View File

@ -151,6 +151,8 @@ func (syncer *Syncer) initAdapter() {
var dataSourceName string var dataSourceName string
if syncer.DatabaseType == "mssql" { if syncer.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database) dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else if syncer.DatabaseType == "postgres" {
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else { } else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port) dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
} }

View File

@ -173,16 +173,21 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
} }
for _, tableColumn := range syncer.TableColumns { for _, tableColumn := range syncer.TableColumns {
tableColumnName := tableColumn.Name
if syncer.Type == "Keycloak" && syncer.DatabaseType == "postgres" {
tableColumnName = strings.ToLower(tableColumnName)
}
value := "" value := ""
if strings.Contains(tableColumn.Name, "+") { if strings.Contains(tableColumnName, "+") {
names := strings.Split(tableColumn.Name, "+") names := strings.Split(tableColumnName, "+")
var values []string var values []string
for _, name := range names { for _, name := range names {
values = append(values, result[strings.Trim(name, " ")]) values = append(values, result[strings.Trim(name, " ")])
} }
value = strings.Join(values, " ") value = strings.Join(values, " ")
} else { } else {
value = result[tableColumn.Name] value = result[tableColumnName]
} }
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value) syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value)
} }
@ -209,7 +214,12 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
tm := time.Unix(i/int64(1000), 0) tm := time.Unix(i/int64(1000), 0)
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00") originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
// enable // enable
originalUser.IsForbidden = !(result["ENABLED"] == "\x01") value, ok := result["ENABLED"]
if ok {
originalUser.IsForbidden = !util.ParseBool(value)
} else {
originalUser.IsForbidden = !util.ParseBool(result["enabled"])
}
} }
users = append(users, originalUser) users = append(users, originalUser)

View File

@ -19,12 +19,15 @@ func (syncer *Syncer) getUsers() []*User {
return users return users
} }
func (syncer *Syncer) getUserMap() ([]*User, map[string]*User) { func (syncer *Syncer) getUserMap() ([]*User, map[string]*User, map[string]*User) {
users := syncer.getUsers() users := syncer.getUsers()
m := map[string]*User{} m1 := map[string]*User{}
m2 := map[string]*User{}
for _, user := range users { for _, user := range users {
m[user.Id] = user m1[user.Id] = user
m2[user.Name] = user
} }
return users, m
return users, m1, m2
} }

View File

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

View File

@ -241,11 +241,11 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
samlResponse := NewSamlResponse11(user, request.RequestID, host) samlResponse := NewSamlResponse11(user, request.RequestID, host)
cert := getCertByApplication(application) cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.PublicKey)) block, _ := pem.Decode([]byte(cert.Certificate))
publicKey := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
randomKeyStore := &X509Key{ randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey, PrivateKey: cert.PrivateKey,
X509Certificate: publicKey, X509Certificate: certificate,
} }
ctx := dsig.NewDefaultSigningContext(randomKeyStore) ctx := dsig.NewDefaultSigningContext(randomKeyStore)

View File

@ -129,13 +129,13 @@ func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
// RSA public key // RSA certificate
publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.PublicKey)) certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return publicKey, nil return certificate, nil
}) })
if t != nil { if t != nil {

View File

@ -23,10 +23,10 @@ import (
func TestGenerateRsaKeys(t *testing.T) { func TestGenerateRsaKeys(t *testing.T) {
fileId := "token_jwt_key" fileId := "token_jwt_key"
publicKey, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization") certificate, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
// Write certificate (aka public key) to file. // Write certificate (aka certificate) to file.
util.WriteStringToPath(publicKey, fmt.Sprintf("%s.pem", fileId)) util.WriteStringToPath(certificate, fmt.Sprintf("%s.pem", fileId))
// Write private key to file. // Write private key to file.
util.WriteStringToPath(privateKey, fmt.Sprintf("%s.key", fileId)) util.WriteStringToPath(privateKey, fmt.Sprintf("%s.key", fileId))

View File

@ -20,6 +20,7 @@ import (
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn"
"xorm.io/core" "xorm.io/core"
) )
@ -72,7 +73,7 @@ type User struct {
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"` LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"` LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"`
Github string `xorm:"varchar(100)" json:"github"` GitHub string `xorm:"github varchar(100)" json:"github"`
Google string `xorm:"varchar(100)" json:"google"` Google string `xorm:"varchar(100)" json:"google"`
QQ string `xorm:"qq varchar(100)" json:"qq"` QQ string `xorm:"qq varchar(100)" json:"qq"`
WeChat string `xorm:"wechat varchar(100)" json:"wechat"` WeChat string `xorm:"wechat varchar(100)" json:"wechat"`
@ -96,11 +97,16 @@ type User struct {
Steam string `xorm:"steam varchar(100)" json:"steam"` Steam string `xorm:"steam varchar(100)" json:"steam"`
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"` Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Okta string `xorm:"okta varchar(100)" json:"okta"` Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin vachar(100)" json:"douyin"` Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
Custom string `xorm:"custom varchar(100)" json:"custom"` Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"` Properties map[string]string `json:"properties"`
Roles []*Role `json:"roles"`
Permissions []*Permission `json:"permissions"`
} }
type Userinfo struct { type Userinfo struct {
@ -267,6 +273,42 @@ func GetUserByEmail(owner string, email string) *User {
} }
} }
func GetUserByPhone(owner string, phone string) *User {
if owner == "" || phone == "" {
return nil
}
user := User{Owner: owner, Phone: phone}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}
func GetUserByUserId(owner string, userId string) *User {
if owner == "" || userId == "" {
return nil
}
user := User{Owner: owner, Id: userId}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}
func GetUser(id string) *User { func GetUser(id string) *User {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
return getUser(owner, name) return getUser(owner, name)
@ -326,9 +368,11 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
} }
if len(columns) == 0 { if len(columns) == 0 {
columns = []string{"owner", "display_name", "avatar", columns = []string{
"owner", "display_name", "avatar",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application", "location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties"} "is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials",
}
} }
if isGlobalAdmin { if isGlobalAdmin {
columns = append(columns, "name", "email", "phone") columns = append(columns, "name", "email", "phone")

View File

@ -37,11 +37,11 @@ func TestSyncAvatarsFromGitHub(t *testing.T) {
users := GetGlobalUsers() users := GetGlobalUsers()
for _, user := range users { for _, user := range users {
if user.Github == "" { if user.GitHub == "" {
continue continue
} }
user.Avatar = fmt.Sprintf("https://avatars.githubusercontent.com/%s", user.Github) user.Avatar = fmt.Sprintf("https://avatars.githubusercontent.com/%s", user.GitHub)
updateUserColumn("avatar", user) updateUserColumn("avatar", user)
} }
} }

View File

@ -106,6 +106,10 @@ func setUserProperty(user *User, field string, value string) {
if value == "" { if value == "" {
delete(user.Properties, field) delete(user.Properties, field)
} else { } else {
if user.Properties == nil {
user.Properties = make(map[string]string)
}
user.Properties[field] = value user.Properties[field] = value
} }
} }

102
object/user_webauthn.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"encoding/base64"
"net/url"
"strings"
"github.com/astaxie/beego"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
func GetWebAuthnObject(host string) *webauthn.WebAuthn {
var err error
origin := beego.AppConfig.String("origin")
if origin == "" {
_, origin = getOriginFromHost(host)
}
localUrl, err := url.Parse(origin)
if err != nil {
panic("error when parsing origin:" + err.Error())
}
webAuthn, err := webauthn.New(&webauthn.Config{
RPDisplayName: beego.AppConfig.String("appname"), // Display Name for your site
RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array
RPOrigin: origin, // The origin URL for WebAuthn requests
// RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
})
if err != nil {
panic(err)
}
return webAuthn
}
// implementation of webauthn.User interface
func (u *User) WebAuthnID() []byte {
return []byte(u.GetId())
}
func (u *User) WebAuthnName() string {
return u.Name
}
func (u *User) WebAuthnDisplayName() string {
return u.DisplayName
}
func (u *User) WebAuthnCredentials() []webauthn.Credential {
return u.WebauthnCredentials
}
func (u *User) WebAuthnIcon() string {
return u.Avatar
}
// CredentialExcludeList returns a CredentialDescriptor array filled with all the user's credentials
func (u *User) CredentialExcludeList() []protocol.CredentialDescriptor {
credentials := u.WebAuthnCredentials()
credentialExcludeList := []protocol.CredentialDescriptor{}
for _, cred := range credentials {
descriptor := protocol.CredentialDescriptor{
Type: protocol.PublicKeyCredentialType,
CredentialID: cred.ID,
}
credentialExcludeList = append(credentialExcludeList, descriptor)
}
return credentialExcludeList
}
func (u *User) AddCredentials(credential webauthn.Credential, isGlobalAdmin bool) bool {
u.WebauthnCredentials = append(u.WebauthnCredentials, credential)
return UpdateUser(u.GetId(), u, []string{"webauthnCredentials"}, isGlobalAdmin)
}
func (u *User) DeleteCredentials(credentialIdBase64 string) bool {
for i, credential := range u.WebauthnCredentials {
if base64.StdEncoding.EncodeToString(credential.ID) == credentialIdBase64 {
u.WebauthnCredentials = append(u.WebauthnCredentials[0:i], u.WebauthnCredentials[i+1:]...)
return UpdateUserForAllFields(u.GetId(), u)
}
}
return false
}

View File

@ -28,7 +28,7 @@ type AlipayPaymentProvider struct {
Client *alipay.Client Client *alipay.Client
} }
func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider { func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider {
pp := &AlipayPaymentProvider{} pp := &AlipayPaymentProvider{}
client, err := alipay.NewClient(appId, appPrivateKey, true) client, err := alipay.NewClient(appId, appPrivateKey, true)
@ -36,7 +36,7 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
panic(err) panic(err)
} }
err = client.SetCertSnByContent([]byte(appPublicKey), []byte(authorityRootPublicKey), []byte(authorityPublicKey)) err = client.SetCertSnByContent([]byte(appCertificate), []byte(authorityRootPublicKey), []byte(authorityPublicKey))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -22,9 +22,9 @@ type PaymentProvider interface {
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
} }
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider { func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {
if typ == "Alipay" { if typ == "Alipay" {
return NewAlipayPaymentProvider(appId, appPublicKey, appPrivateKey, authorityPublicKey, authorityRootPublicKey) return NewAlipayPaymentProvider(appId, appCertificate, appPrivateKey, authorityPublicKey, authorityRootPublicKey)
} else if typ == "GC" { } else if typ == "GC" {
return NewGcPaymentProvider(appId, clientSecret, host) return NewGcPaymentProvider(appId, clientSecret, host)
} }

View File

@ -54,17 +54,17 @@ func isAddressOpen(address string) bool {
} }
func getProxyHttpClient() *http.Client { func getProxyHttpClient() *http.Client {
sock5Proxy := conf.GetConfigString("sock5Proxy") socks5Proxy := conf.GetConfigString("socks5Proxy")
if sock5Proxy == "" { if socks5Proxy == "" {
return &http.Client{} return &http.Client{}
} }
if !isAddressOpen(sock5Proxy) { if !isAddressOpen(socks5Proxy) {
return &http.Client{} return &http.Client{}
} }
// https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client // https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client
dialer, err := proxy.SOCKS5("tcp", sock5Proxy, nil, proxy.Direct) dialer, err := proxy.SOCKS5("tcp", socks5Proxy, nil, proxy.Direct)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -76,7 +76,7 @@ func getProxyHttpClient() *http.Client {
} }
func GetHttpClient(url string) *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 return ProxyHttpClient
} else { } else {
return DefaultHttpClient return DefaultHttpClient

View File

@ -109,6 +109,10 @@ func getUrlPath(urlPath string) string {
return "/api/login/oauth" return "/api/login/oauth"
} }
if strings.HasPrefix(urlPath, "/api/webauthn") {
return "/api/webauthn"
}
return urlPath return urlPath
} }
@ -118,6 +122,10 @@ func AuthzFilter(ctx *context.Context) {
urlPath := getUrlPath(ctx.Request.URL.Path) urlPath := getUrlPath(ctx.Request.URL.Path)
objOwner, objName := getObject(ctx) objOwner, objName := getObject(ctx)
if strings.HasPrefix(urlPath, "/api/notify-payment") {
urlPath = "/api/notify-payment"
}
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName) isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName)
result := "deny" result := "deny"

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

@ -48,7 +48,7 @@ func initAPI() {
beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup") beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup")
beego.Router("/api/login", &controllers.ApiController{}, "POST:Login") beego.Router("/api/login", &controllers.ApiController{}, "POST:Login")
beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin") beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin")
beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout") beego.Router("/api/logout", &controllers.ApiController{}, "GET,POST:Logout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount") beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo") beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo")
beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink") beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink")
@ -191,4 +191,9 @@ func initAPI() {
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate") beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate") beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "Get:WebAuthnSignupBegin")
beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "Post:WebAuthnSignupFinish")
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin")
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish")
} }

View File

@ -2194,6 +2194,18 @@
"schema": { "schema": {
"$ref": "#/definitions/object.TokenWrapper" "$ref": "#/definitions/object.TokenWrapper"
} }
},
"400": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.TokenError"
}
},
"401": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.TokenError"
}
} }
} }
} }
@ -2285,6 +2297,18 @@
"schema": { "schema": {
"$ref": "#/definitions/object.IntrospectionResponse" "$ref": "#/definitions/object.IntrospectionResponse"
} }
},
"400": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.TokenError"
}
},
"401": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.TokenError"
}
} }
} }
} }
@ -2377,11 +2401,38 @@
"schema": { "schema": {
"$ref": "#/definitions/object.TokenWrapper" "$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": { "/api/logout": {
"get": {
"tags": [
"Login API"
],
"description": "logout the current user",
"operationId": "ApiController.Logout",
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
},
"post": { "post": {
"tags": [ "tags": [
"Login API" "Login API"
@ -3060,14 +3111,120 @@
], ],
"operationId": "ApiController.VerifyCaptcha" "operationId": "ApiController.VerifyCaptcha"
} }
},
"/api/webauthn/signin/begin": {
"get": {
"tags": [
"Login API"
],
"description": "WebAuthn Login Flow 1st stage",
"operationId": "ApiController.WebAuthnSigninBegin",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "owner",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "name",
"description": "name",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The CredentialAssertion object",
"schema": {
"$ref": "#/definitions/protocol.CredentialAssertion"
}
}
}
}
},
"/api/webauthn/signin/finish": {
"post": {
"tags": [
"Login API"
],
"description": "WebAuthn Login Flow 2nd stage",
"operationId": "ApiController.WebAuthnSigninBegin",
"parameters": [
{
"in": "body",
"name": "body",
"description": "authenticator assertion Response",
"required": true,
"schema": {
"$ref": "#/definitions/protocol.CredentialAssertionResponse"
}
}
],
"responses": {
"200": {
"description": "\"The Response object\"",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
}
},
"/api/webauthn/signup/begin": {
"get": {
"tags": [
"User API"
],
"description": "WebAuthn Registration Flow 1st stage",
"operationId": "ApiController.WebAuthnSignupBegin",
"responses": {
"200": {
"description": "The CredentialCreationOptions object",
"schema": {
"$ref": "#/definitions/protocol.CredentialCreation"
}
}
}
}
},
"/api/webauthn/signup/finish": {
"post": {
"tags": [
"User API"
],
"description": "WebAuthn Registration Flow 2nd stage",
"operationId": "ApiController.WebAuthnSignupFinish",
"parameters": [
{
"in": "body",
"name": "body",
"description": "authenticator attestation Response",
"required": true,
"schema": {
"$ref": "#/definitions/protocol.CredentialCreationResponse"
}
}
],
"responses": {
"200": {
"description": "\"The Response object\"",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
"2200.0xc0003c4b70.false": { "2127.0xc000427560.false": {
"title": "false", "title": "false",
"type": "object" "type": "object"
}, },
"2235.0xc0003c4ba0.false": { "2161.0xc000427590.false": {
"title": "false", "title": "false",
"type": "object" "type": "object"
}, },
@ -3082,6 +3239,9 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"provider": {
"type": "string"
},
"receivers": { "receivers": {
"type": "array", "type": "array",
"items": { "items": {
@ -3182,10 +3342,10 @@
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {
"$ref": "#/definitions/2200.0xc0003c4b70.false" "$ref": "#/definitions/2127.0xc000427560.false"
}, },
"data2": { "data2": {
"$ref": "#/definitions/2235.0xc0003c4ba0.false" "$ref": "#/definitions/2161.0xc000427590.false"
}, },
"msg": { "msg": {
"type": "string" "type": "string"
@ -3290,12 +3450,18 @@
"enablePassword": { "enablePassword": {
"type": "boolean" "type": "boolean"
}, },
"enableSamlCompress": {
"type": "boolean"
},
"enableSignUp": { "enableSignUp": {
"type": "boolean" "type": "boolean"
}, },
"enableSigninSession": { "enableSigninSession": {
"type": "boolean" "type": "boolean"
}, },
"enableWebAuthn": {
"type": "boolean"
},
"expireInHours": { "expireInHours": {
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
@ -3405,7 +3571,7 @@
"privateKey": { "privateKey": {
"type": "string" "type": "string"
}, },
"publicKey": { "certificate": {
"type": "string" "type": "string"
}, },
"scope": { "scope": {
@ -4209,6 +4375,18 @@
} }
} }
}, },
"object.TokenError": {
"title": "TokenError",
"type": "object",
"properties": {
"error": {
"type": "string"
},
"error_description": {
"type": "string"
}
}
},
"object.TokenWrapper": { "object.TokenWrapper": {
"title": "TokenWrapper", "title": "TokenWrapper",
"type": "object", "type": "object",
@ -4216,9 +4394,6 @@
"access_token": { "access_token": {
"type": "string" "type": "string"
}, },
"error": {
"type": "string"
},
"expires_in": { "expires_in": {
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
@ -4459,6 +4634,12 @@
"updatedTime": { "updatedTime": {
"type": "string" "type": "string"
}, },
"webauthnCredentials": {
"type": "array",
"items": {
"$ref": "#/definitions/webauthn.Credential"
}
},
"wechat": { "wechat": {
"type": "string" "type": "string"
}, },
@ -4548,6 +4729,26 @@
} }
} }
}, },
"protocol.CredentialAssertion": {
"title": "CredentialAssertion",
"type": "object"
},
"protocol.CredentialAssertionResponse": {
"title": "CredentialAssertionResponse",
"type": "object"
},
"protocol.CredentialCreation": {
"title": "CredentialCreation",
"type": "object"
},
"protocol.CredentialCreationResponse": {
"title": "CredentialCreationResponse",
"type": "object"
},
"webauthn.Credential": {
"title": "Credential",
"type": "object"
},
"xorm.Engine": { "xorm.Engine": {
"title": "Engine", "title": "Engine",
"type": "object" "type": "object"

View File

@ -1435,6 +1435,14 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/object.TokenWrapper' $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: /api/login/oauth/code:
post: post:
tags: tags:
@ -1497,6 +1505,14 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/object.IntrospectionResponse' $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: /api/login/oauth/logout:
get: get:
tags: tags:
@ -1559,7 +1575,25 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/object.TokenWrapper' $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: /api/logout:
get:
tags:
- Login API
description: logout the current user
operationId: ApiController.Logout
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
post: post:
tags: tags:
- Login API - Login API
@ -2004,11 +2038,80 @@ paths:
tags: tags:
- Verification API - Verification API
operationId: ApiController.VerifyCaptcha operationId: ApiController.VerifyCaptcha
/api/webauthn/signin/begin:
get:
tags:
- Login API
description: WebAuthn Login Flow 1st stage
operationId: ApiController.WebAuthnSigninBegin
parameters:
- in: query
name: owner
description: owner
required: true
type: string
- in: query
name: name
description: name
required: true
type: string
responses:
"200":
description: The CredentialAssertion object
schema:
$ref: '#/definitions/protocol.CredentialAssertion'
/api/webauthn/signin/finish:
post:
tags:
- Login API
description: WebAuthn Login Flow 2nd stage
operationId: ApiController.WebAuthnSigninBegin
parameters:
- in: body
name: body
description: authenticator assertion Response
required: true
schema:
$ref: '#/definitions/protocol.CredentialAssertionResponse'
responses:
"200":
description: '"The Response object"'
schema:
$ref: '#/definitions/Response'
/api/webauthn/signup/begin:
get:
tags:
- User API
description: WebAuthn Registration Flow 1st stage
operationId: ApiController.WebAuthnSignupBegin
responses:
"200":
description: The CredentialCreationOptions object
schema:
$ref: '#/definitions/protocol.CredentialCreation'
/api/webauthn/signup/finish:
post:
tags:
- User API
description: WebAuthn Registration Flow 2nd stage
operationId: ApiController.WebAuthnSignupFinish
parameters:
- in: body
name: body
description: authenticator attestation Response
required: true
schema:
$ref: '#/definitions/protocol.CredentialCreationResponse'
responses:
"200":
description: '"The Response object"'
schema:
$ref: '#/definitions/Response'
definitions: definitions:
2200.0xc0003c4b70.false: 2127.0xc000427560.false:
title: "false" title: "false"
type: object type: object
2235.0xc0003c4ba0.false: 2161.0xc000427590.false:
title: "false" title: "false"
type: object type: object
Response: Response:
@ -2020,6 +2123,8 @@ definitions:
properties: properties:
content: content:
type: string type: string
provider:
type: string
receivers: receivers:
type: array type: array
items: items:
@ -2087,9 +2192,9 @@ definitions:
type: object type: object
properties: properties:
data: data:
$ref: '#/definitions/2200.0xc0003c4b70.false' $ref: '#/definitions/2127.0xc000427560.false'
data2: data2:
$ref: '#/definitions/2235.0xc0003c4ba0.false' $ref: '#/definitions/2161.0xc000427590.false'
msg: msg:
type: string type: string
name: name:
@ -2159,10 +2264,14 @@ definitions:
type: boolean type: boolean
enablePassword: enablePassword:
type: boolean type: boolean
enableSamlCompress:
type: boolean
enableSignUp: enableSignUp:
type: boolean type: boolean
enableSigninSession: enableSigninSession:
type: boolean type: boolean
enableWebAuthn:
type: boolean
expireInHours: expireInHours:
type: integer type: integer
format: int64 format: int64
@ -2237,7 +2346,7 @@ definitions:
type: string type: string
privateKey: privateKey:
type: string type: string
publicKey: certificate:
type: string type: string
scope: scope:
type: string type: string
@ -2776,14 +2885,20 @@ definitions:
type: string type: string
user: user:
type: string type: string
object.TokenError:
title: TokenError
type: object
properties:
error:
type: string
error_description:
type: string
object.TokenWrapper: object.TokenWrapper:
title: TokenWrapper title: TokenWrapper
type: object type: object
properties: properties:
access_token: access_token:
type: string type: string
error:
type: string
expires_in: expires_in:
type: integer type: integer
format: int64 format: int64
@ -2945,6 +3060,10 @@ definitions:
type: string type: string
updatedTime: updatedTime:
type: string type: string
webauthnCredentials:
type: array
items:
$ref: '#/definitions/webauthn.Credential'
wechat: wechat:
type: string type: string
wecom: wecom:
@ -3003,6 +3122,21 @@ definitions:
type: string type: string
url: url:
type: string type: string
protocol.CredentialAssertion:
title: CredentialAssertion
type: object
protocol.CredentialAssertionResponse:
title: CredentialAssertionResponse
type: object
protocol.CredentialCreation:
title: CredentialCreation
type: object
protocol.CredentialCreationResponse:
title: CredentialCreationResponse
type: object
webauthn.Credential:
title: Credential
type: object
xorm.Engine: xorm.Engine:
title: Engine title: Engine
type: object type: object

30
util/crypto.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
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
}

View File

@ -52,8 +52,10 @@ func ParseFloat(s string) float64 {
} }
func ParseBool(s string) bool { func ParseBool(s string) bool {
if s == "\x01" { if s == "\x01" || s == "true" {
return true return true
} else if s == "false" {
return false
} }
i := ParseInt(s) i := ParseInt(s)

2
web/.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
build

63
web/.eslintrc Normal file
View File

@ -0,0 +1,63 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"rules": {
// "eqeqeq": "error",
"semi": ["error", "always"],
// "indent": ["error", 2],
// follow antd's style guide
"quotes": ["error", "double"],
"jsx-quotes": ["error", "prefer-double"],
"space-in-parens": ["error", "never"],
"object-curly-spacing": ["error", "never"],
"array-bracket-spacing": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"react/jsx-curly-spacing": [
"error",
{ "when": "never", "allowMultiline": true, "children": true }
],
"arrow-spacing": ["error", { "before": true, "after": true }],
"space-before-blocks": ["error", "always"],
"spaced-comment": ["error", "always"],
"react/jsx-tag-spacing": ["error", { "beforeSelfClosing": "always" }],
"block-spacing": ["error", "never"],
"space-before-function-paren": ["error", "never"],
"no-trailing-spaces": ["error", { "ignoreComments": true }],
"eol-last": ["error", "always"],
// "no-var": ["error"],
"curly": ["error", "all"],
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"no-mixed-spaces-and-tabs": "error",
"sort-imports": ["error", {
"ignoreDeclarationSort": true
}],
"react/prop-types": "off",
"react/display-name": "off",
"react/react-in-jsx-scope": "off",
// don't use strict mod now, otherwise there are a lot of errors in the codebase
"no-unused-vars": "warn",
"react/no-deprecated": "warn",
"no-case-declarations": "warn",
"react/jsx-key": "warn"
}
}

View File

@ -1,38 +1,38 @@
const CracoLessPlugin = require('craco-less'); const CracoLessPlugin = require("craco-less");
module.exports = { module.exports = {
devServer: { devServer: {
proxy: { proxy: {
'/api': { "/api": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
'/swagger': { "/swagger": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
'/files': { "/files": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
'/.well-known/openid-configuration': { "/.well-known/openid-configuration": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
'/cas/serviceValidate': { "/cas/serviceValidate": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
'/cas/proxyValidate': { "/cas/proxyValidate": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
'/cas/proxy': { "/cas/proxy": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
}, },
'/cas/validate': { "/cas/validate": {
target: 'http://localhost:8000', target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
} }
}, },
@ -43,7 +43,7 @@ module.exports = {
options: { options: {
lessLoaderOptions: { lessLoaderOptions: {
lessOptions: { lessOptions: {
modifyVars: {'@primary-color': 'rgb(45,120,213)'}, modifyVars: {"@primary-color": "rgb(45,120,213)"},
javascriptEnabled: true, javascriptEnabled: true,
}, },
}, },

View File

@ -59,6 +59,8 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"cross-env": "^7.0.3" "cross-env": "^7.0.3",
"eslint": "^7.11.0",
"eslint-plugin-react": "^7.30.1"
} }
} }

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined} from '@ant-design/icons'; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Switch, Table, Tooltip} from 'antd'; import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
@ -65,8 +65,8 @@ class AccountTable extends React.Component {
const columns = [ const columns = [
{ {
title: i18next.t("provider:Name"), title: i18next.t("provider:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
render: (text, record, index) => { render: (text, record, index) => {
const items = [ const items = [
{name: "Organization", displayName: i18next.t("general:Organization")}, {name: "Organization", displayName: i18next.t("general:Organization")},
@ -86,12 +86,15 @@ class AccountTable extends React.Component {
{name: "Bio", displayName: i18next.t("user:Bio")}, {name: "Bio", displayName: i18next.t("user:Bio")},
{name: "Tag", displayName: i18next.t("user:Tag")}, {name: "Tag", displayName: i18next.t("user:Tag")},
{name: "Signup application", displayName: i18next.t("general:Signup application")}, {name: "Signup application", displayName: i18next.t("general:Signup application")},
{name: "Roles", displayName: i18next.t("general:Roles")},
{name: "Permissions", displayName: i18next.t("general:Permissions")},
{name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")}, {name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")},
{name: "Properties", displayName: i18next.t("user:Properties")}, {name: "Properties", displayName: i18next.t("user:Properties")},
{name: "Is admin", displayName: i18next.t("user:Is admin")}, {name: "Is admin", displayName: i18next.t("user:Is admin")},
{name: "Is global admin", displayName: i18next.t("user:Is global admin")}, {name: "Is global admin", displayName: i18next.t("user:Is global admin")},
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")}, {name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")}, {name: "Is deleted", displayName: i18next.t("user:Is deleted")},
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
]; ];
const getItemDisplayName = (text) => { const getItemDisplayName = (text) => {
@ -103,63 +106,63 @@ class AccountTable extends React.Component {
}; };
return ( return (
<Select virtual={false} style={{width: '100%'}} <Select virtual={false} style={{width: "100%"}}
value={getItemDisplayName(text)} value={getItemDisplayName(text)}
onChange={value => { onChange={value => {
this.updateField(table, index, 'name', value); this.updateField(table, index, "name", value);
}} > }} >
{ {
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>) Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>)
} }
</Select> </Select>
) );
} }
}, },
{ {
title: i18next.t("provider:visible"), title: i18next.t("provider:visible"),
dataIndex: 'visible', dataIndex: "visible",
key: 'visible', key: "visible",
width: '120px', width: "120px",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Switch checked={text} onChange={checked => { <Switch checked={text} onChange={checked => {
this.updateField(table, index, 'visible', checked); this.updateField(table, index, "visible", checked);
}} /> }} />
) );
} }
}, },
{ {
title: i18next.t("organization:viewRule"), title: i18next.t("organization:viewRule"),
dataIndex: 'viewRule', dataIndex: "viewRule",
key: 'viewRule', key: "viewRule",
width: '155px', width: "155px",
render: (text, record, index) => { render: (text, record, index) => {
if (!record.visible) { if (!record.visible) {
return null; return null;
} }
let options = [ let options = [
{id: 'Public', name: 'Public'}, {id: "Public", name: "Public"},
{id: 'Self', name: 'Self'}, {id: "Self", name: "Self"},
{id: 'Admin', name: 'Admin'}, {id: "Admin", name: "Admin"},
]; ];
return ( return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={text} onChange={(value => {
this.updateField(table, index, 'viewRule', value); this.updateField(table, index, "viewRule", value);
})}> })}>
{ {
options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
) );
} }
}, },
{ {
title: i18next.t("organization:modifyRule"), title: i18next.t("organization:modifyRule"),
dataIndex: 'modifyRule', dataIndex: "modifyRule",
key: 'modifyRule', key: "modifyRule",
width: '155px', width: "155px",
render: (text, record, index) => { render: (text, record, index) => {
if (!record.visible) { if (!record.visible) {
return null; return null;
@ -168,32 +171,32 @@ class AccountTable extends React.Component {
let options; let options;
if (record.viewRule === "Admin") { if (record.viewRule === "Admin") {
options = [ options = [
{id: 'Admin', name: 'Admin'}, {id: "Admin", name: "Admin"},
{id: 'Immutable', name: 'Immutable'}, {id: "Immutable", name: "Immutable"},
]; ];
} else { } else {
options = [ options = [
{id: 'Self', name: 'Self'}, {id: "Self", name: "Self"},
{id: 'Admin', name: 'Admin'}, {id: "Admin", name: "Admin"},
{id: 'Immutable', name: 'Immutable'}, {id: "Immutable", name: "Immutable"},
]; ];
} }
return ( return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={text} onChange={(value => {
this.updateField(table, index, 'modifyRule', value); this.updateField(table, index, "modifyRule", value);
})}> })}>
{ {
options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
) );
} }
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
key: 'action', key: "action",
width: '100px', width: "100px",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
@ -213,7 +216,7 @@ class AccountTable extends React.Component {
]; ];
return ( return (
<Table scroll={{x: 'max-content'}} rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false} <Table scroll={{x: "max-content"}} rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => ( title={() => (
<div> <div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp; {this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
@ -227,7 +230,7 @@ class AccountTable extends React.Component {
render() { render() {
return ( return (
<div> <div>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col span={24}> <Col span={24}>
{ {
this.renderTable(this.props.table) this.renderTable(this.props.table)
@ -235,7 +238,7 @@ class AccountTable extends React.Component {
</Col> </Col>
</Row> </Row>
</div> </div>
) );
} }
} }

View File

@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React, {Component} from 'react'; import React, {Component} from "react";
import './App.less'; import "./App.less";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {DownOutlined, LogoutOutlined, SettingOutlined} from '@ant-design/icons'; import {DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
import {Avatar, BackTop, Dropdown, Layout, Menu, Card, Result, Button} from 'antd'; import {Avatar, BackTop, Button, Card, Dropdown, Layout, Menu, Result} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from 'react-router-dom' import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage"; import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage"; import OrganizationEditPage from "./OrganizationEditPage";
import UserListPage from "./UserListPage"; import UserListPage from "./UserListPage";
@ -63,11 +63,11 @@ import SelfForgetPage from "./auth/SelfForgetPage";
import ForgetPage from "./auth/ForgetPage"; import ForgetPage from "./auth/ForgetPage";
import * as AuthBackend from "./auth/AuthBackend"; import * as AuthBackend from "./auth/AuthBackend";
import AuthCallback from "./auth/AuthCallback"; import AuthCallback from "./auth/AuthCallback";
import SelectLanguageBox from './SelectLanguageBox'; import SelectLanguageBox from "./SelectLanguageBox";
import i18next from 'i18next'; import i18next from "i18next";
import PromptPage from "./auth/PromptPage"; import PromptPage from "./auth/PromptPage";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage"; import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from './auth/SamlCallback'; import SamlCallback from "./auth/SamlCallback";
import CasLogout from "./auth/CasLogout"; import CasLogout from "./auth/CasLogout";
import ModelListPage from "./ModelListPage"; import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage"; import ModelEditPage from "./ModelEditPage";
@ -110,44 +110,44 @@ class App extends Component {
this.setState({ this.setState({
uri: uri, uri: uri,
}); });
if (uri === '/') { if (uri === "/") {
this.setState({ selectedMenuKey: '/' }); this.setState({selectedMenuKey: "/"});
} else if (uri.includes('/organizations')) { } else if (uri.includes("/organizations")) {
this.setState({ selectedMenuKey: '/organizations' }); this.setState({selectedMenuKey: "/organizations"});
} else if (uri.includes('/users')) { } else if (uri.includes("/users")) {
this.setState({ selectedMenuKey: '/users' }); this.setState({selectedMenuKey: "/users"});
} else if (uri.includes('/roles')) { } else if (uri.includes("/roles")) {
this.setState({ selectedMenuKey: '/roles' }); this.setState({selectedMenuKey: "/roles"});
} else if (uri.includes('/permissions')) { } else if (uri.includes("/permissions")) {
this.setState({ selectedMenuKey: '/permissions' }); this.setState({selectedMenuKey: "/permissions"});
} else if (uri.includes('/models')) { } else if (uri.includes("/models")) {
this.setState({ selectedMenuKey: '/models' }); this.setState({selectedMenuKey: "/models"});
} else if (uri.includes('/providers')) { } else if (uri.includes("/providers")) {
this.setState({ selectedMenuKey: '/providers' }); this.setState({selectedMenuKey: "/providers"});
} else if (uri.includes('/applications')) { } else if (uri.includes("/applications")) {
this.setState({ selectedMenuKey: '/applications' }); this.setState({selectedMenuKey: "/applications"});
} else if (uri.includes('/resources')) { } else if (uri.includes("/resources")) {
this.setState({ selectedMenuKey: '/resources' }); this.setState({selectedMenuKey: "/resources"});
} else if (uri.includes('/tokens')) { } else if (uri.includes("/tokens")) {
this.setState({ selectedMenuKey: '/tokens' }); this.setState({selectedMenuKey: "/tokens"});
} else if (uri.includes('/records')) { } else if (uri.includes("/records")) {
this.setState({ selectedMenuKey: '/records' }); this.setState({selectedMenuKey: "/records"});
} else if (uri.includes('/webhooks')) { } else if (uri.includes("/webhooks")) {
this.setState({ selectedMenuKey: '/webhooks' }); this.setState({selectedMenuKey: "/webhooks"});
} else if (uri.includes('/syncers')) { } else if (uri.includes("/syncers")) {
this.setState({ selectedMenuKey: '/syncers' }); this.setState({selectedMenuKey: "/syncers"});
} else if (uri.includes('/certs')) { } else if (uri.includes("/certs")) {
this.setState({ selectedMenuKey: '/certs' }); this.setState({selectedMenuKey: "/certs"});
} else if (uri.includes('/products')) { } else if (uri.includes("/products")) {
this.setState({ selectedMenuKey: '/products' }); this.setState({selectedMenuKey: "/products"});
} else if (uri.includes('/payments')) { } else if (uri.includes("/payments")) {
this.setState({ selectedMenuKey: '/payments' }); this.setState({selectedMenuKey: "/payments"});
} else if (uri.includes('/signup')) { } else if (uri.includes("/signup")) {
this.setState({ selectedMenuKey: '/signup' }); this.setState({selectedMenuKey: "/signup"});
} else if (uri.includes('/login')) { } else if (uri.includes("/login")) {
this.setState({ selectedMenuKey: '/login' }); this.setState({selectedMenuKey: "/login"});
} else if (uri.includes('/result')) { } else if (uri.includes("/result")) {
this.setState({ selectedMenuKey: '/result' }); this.setState({selectedMenuKey: "/result"});
} else { } else {
this.setState({selectedMenuKey: -1}); this.setState({selectedMenuKey: -1});
} }
@ -234,15 +234,19 @@ class App extends Component {
AuthBackend.logout() AuthBackend.logout()
.then((res) => { .then((res) => {
if (res.status === 'ok') { if (res.status === "ok") {
const owner = this.state.account.owner;
this.setState({ this.setState({
account: null account: null
}); });
Setting.showMessage("success", `Logged out successfully`); Setting.showMessage("success", "Logged out successfully");
let redirectUri = res.data2; let redirectUri = res.data2;
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") { if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
Setting.goToLink(redirectUri); Setting.goToLink(redirectUri);
} else if (owner !== "built-in") {
Setting.goToLink(`${window.location.origin}/login/${owner}`);
} else { } else {
Setting.goToLinkSoft(this, "/"); Setting.goToLinkSoft(this, "/");
} }
@ -259,9 +263,9 @@ class App extends Component {
} }
handleRightDropdownClick(e) { handleRightDropdownClick(e) {
if (e.key === '/account') { if (e.key === "/account") {
this.props.history.push(`/account`); this.props.history.push("/account");
} else if (e.key === '/logout') { } else if (e.key === "/logout") {
this.logout(); this.logout();
} }
} }
@ -269,16 +273,16 @@ class App extends Component {
renderAvatar() { renderAvatar() {
if (this.state.account.avatar === "") { if (this.state.account.avatar === "") {
return ( return (
<Avatar style={{ backgroundColor: Setting.getAvatarColor(this.state.account.name), verticalAlign: 'middle' }} size="large"> <Avatar style={{backgroundColor: Setting.getAvatarColor(this.state.account.name), verticalAlign: "middle"}} size="large">
{Setting.getShortName(this.state.account.name)} {Setting.getShortName(this.state.account.name)}
</Avatar> </Avatar>
) );
} else { } else {
return ( return (
<Avatar src={this.state.account.avatar} style={{verticalAlign: 'middle' }} size="large"> <Avatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size="large">
{Setting.getShortName(this.state.account.name)} {Setting.getShortName(this.state.account.name)}
</Avatar> </Avatar>
) );
} }
} }
@ -287,10 +291,12 @@ class App extends Component {
<Menu onClick={this.handleRightDropdownClick.bind(this)}> <Menu onClick={this.handleRightDropdownClick.bind(this)}>
<Menu.Item key="/account"> <Menu.Item key="/account">
<SettingOutlined /> <SettingOutlined />
&nbsp;
{i18next.t("account:My Account")} {i18next.t("account:My Account")}
</Menu.Item> </Menu.Item>
<Menu.Item key="/logout"> <Menu.Item key="/logout">
<LogoutOutlined /> <LogoutOutlined />
&nbsp;
{i18next.t("account:Logout")} {i18next.t("account:Logout")}
</Menu.Item> </Menu.Item>
</Menu> </Menu>
@ -298,7 +304,7 @@ class App extends Component {
return ( return (
<Dropdown key="/rightDropDown" overlay={menu} className="rightDropDown"> <Dropdown key="/rightDropDown" overlay={menu} className="rightDropDown">
<div className="ant-dropdown-link" style={{float: 'right', cursor: 'pointer'}}> <div className="ant-dropdown-link" style={{float: "right", cursor: "pointer"}}>
&nbsp; &nbsp;
&nbsp; &nbsp;
{ {
@ -312,7 +318,7 @@ class App extends Component {
&nbsp; &nbsp;
</div> </div>
</Dropdown> </Dropdown>
) );
} }
renderAccount() { renderAccount() {
@ -484,7 +490,7 @@ class App extends Component {
renderHomeIfLoggedIn(component) { renderHomeIfLoggedIn(component) {
if (this.state.account !== null && this.state.account !== undefined) { if (this.state.account !== null && this.state.account !== undefined) {
return <Redirect to='/' /> return <Redirect to="/" />;
} else { } else {
return component; return component;
} }
@ -493,19 +499,18 @@ class App extends Component {
renderLoginIfNotLoggedIn(component) { renderLoginIfNotLoggedIn(component) {
if (this.state.account === null) { if (this.state.account === null) {
sessionStorage.setItem("from", window.location.pathname); sessionStorage.setItem("from", window.location.pathname);
return <Redirect to='/login' /> return <Redirect to="/login" />;
} else if (this.state.account === undefined) { } else if (this.state.account === undefined) {
return null; return null;
} } else {
else {
return component; return component;
} }
} }
isStartPages() { isStartPages() {
return window.location.pathname.startsWith('/login') || return window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith('/signup') || window.location.pathname.startsWith("/signup") ||
window.location.pathname === '/'; window.location.pathname === "/";
} }
renderRouter() { renderRouter() {
@ -555,15 +560,15 @@ class App extends Component {
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} /> extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch> </Switch>
</div> </div>
) );
} }
renderContent() { renderContent() {
if (!Setting.isMobile()) { if (!Setting.isMobile()) {
return ( return (
<div style={{display: 'flex', flex: 'auto',width:"100%",flexDirection: 'column'}}> <div style={{display: "flex", flex: "auto", width:"100%", flexDirection: "column"}}>
<Layout style={{display: 'flex', alignItems: 'stretch'}}> <Layout style={{display: "flex", alignItems: "stretch"}}>
<Header style={{ padding: '0', marginBottom: '3px'}}> <Header style={{padding: "0", marginBottom: "3px"}}>
{ {
Setting.isMobile() ? null : ( Setting.isMobile() ? null : (
<Link to={"/"}> <Link to={"/"}>
@ -576,7 +581,7 @@ class App extends Component {
// theme="dark" // theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"} mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]} selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: '64px', width: '80%', position: 'absolute'}} style={{lineHeight: "64px", width: "80%", position: "absolute"}}
> >
{ {
this.renderMenu() this.renderMenu()
@ -588,7 +593,7 @@ class App extends Component {
<SelectLanguageBox /> <SelectLanguageBox />
</div> </div>
</Header> </Header>
<Layout style={{backgroundColor: "#f5f5f5", alignItems: 'stretch'}}> <Layout style={{backgroundColor: "#f5f5f5", alignItems: "stretch"}}>
<Card className="content-warp-card"> <Card className="content-warp-card">
{ {
this.renderRouter() this.renderRouter()
@ -597,11 +602,11 @@ class App extends Component {
</Layout> </Layout>
</Layout> </Layout>
</div> </div>
) );
} else { } else {
return( return(
<div> <div>
<Header style={{ padding: '0', marginBottom: '3px'}}> <Header style={{padding: "0", marginBottom: "3px"}}>
{ {
Setting.isMobile() ? null : ( Setting.isMobile() ? null : (
<Link to={"/"}> <Link to={"/"}>
@ -613,12 +618,12 @@ class App extends Component {
// theme="dark" // theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"} mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]} selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{ lineHeight: '64px' }} style={{lineHeight: "64px"}}
> >
{ {
this.renderMenu() this.renderMenu()
} }
<div style = {{float: 'right'}}> <div style = {{float: "right"}}>
{ {
this.renderAccount() this.renderAccount()
} }
@ -630,7 +635,7 @@ class App extends Component {
this.renderRouter() this.renderRouter()
} }
</div> </div>
) );
} }
} }
@ -641,14 +646,14 @@ class App extends Component {
return ( return (
<Footer id="footer" style={ <Footer id="footer" style={
{ {
borderTop: '1px solid #e8e8e8', borderTop: "1px solid #e8e8e8",
backgroundColor: 'white', backgroundColor: "white",
textAlign: 'center', textAlign: "center",
} }
}> }>
Made with <span style={{color: 'rgb(255, 255, 255)'}}></span> by <a style={{fontWeight: "bold", color: "black"}} target="_blank" href="https://casdoor.org" rel="noreferrer">Casdoor</a> Made with <span style={{color: "rgb(255, 255, 255)"}}></span> by <a style={{fontWeight: "bold", color: "black"}} target="_blank" href="https://casdoor.org" rel="noreferrer">Casdoor</a>
</Footer> </Footer>
) );
} }
isDoorPages() { isDoorPages() {
@ -663,25 +668,32 @@ class App extends Component {
renderPage() { renderPage() {
if (this.isDoorPages()) { if (this.isDoorPages()) {
return ( return (
<div>
<Switch> <Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} /> <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="/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="/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/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
<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="/auto-signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...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="/signup/oauth/authorize" render={(props) => <SignupPage account={this.state.account} {...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/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="/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" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} /> <Route exact path="/callback/saml" component={SamlCallback} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)} /> <Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...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" 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 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.")} <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>} />} /> extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch> </Switch>
) {
this.renderFooter()
}
</div>
);
} }
return ( return (
@ -711,7 +723,7 @@ class App extends Component {
this.renderPage() this.renderPage()
} }
</React.Fragment> </React.Fragment>
) );
} }
const organization = this.state.account.organization; const organization = this.state.account.organization;
@ -725,7 +737,7 @@ class App extends Component {
this.renderPage() this.renderPage()
} }
</React.Fragment> </React.Fragment>
) );
} }
} }

View File

@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React from 'react'; import React from "react";
import { render } from '@testing-library/react'; import {render} from "@testing-library/react";
import App from './App'; import App from "./App";
test('renders learn react link', () => { // eslint-disable-next-line no-undef
test("renders learn react link", () => {
const {getByText} = render(<App />); const {getByText} = render(<App />);
const linkElement = getByText(/learn react/i); const linkElement = getByText(/learn react/i);
// eslint-disable-next-line no-undef
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument();
}); });

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from 'antd'; import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from "antd";
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons"; import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
@ -30,9 +30,9 @@ import SignupTable from "./SignupTable";
import PromptPage from "./auth/PromptPage"; import PromptPage from "./auth/PromptPage";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import {Controlled as CodeMirror} from 'react-codemirror2'; import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
require('codemirror/theme/material-darker.css'); require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed"); require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml"); require("codemirror/mode/xml/xml");
@ -106,7 +106,7 @@ class ApplicationEditPage extends React.Component {
.then((res) => { .then((res) => {
this.setState({ this.setState({
samlMetadata: res, samlMetadata: res,
}) });
}); });
} }
@ -144,7 +144,7 @@ class ApplicationEditPage extends React.Component {
} }
}).finally(() => { }).finally(() => {
this.setState({uploading: false}); this.setState({uploading: false});
}) });
} }
renderApplication() { renderApplication() {
@ -153,248 +153,258 @@ class ApplicationEditPage extends React.Component {
<div> <div>
{this.state.mode === "add" ? i18next.t("application:New Application") : i18next.t("application:Edit Application")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("application:New Application") : i18next.t("application:Edit Application")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> } style={(Setting.isMobile())? {margin: "5px"}:{}} type="inner">
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: "10px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.application.name} disabled={this.state.application.name === "app-built-in"} onChange={e => { <Input value={this.state.application.name} disabled={this.state.application.name === "app-built-in"} onChange={e => {
this.updateApplicationField('name', e.target.value); this.updateApplicationField("name", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.application.displayName} onChange={e => { <Input value={this.state.application.displayName} onChange={e => {
this.updateApplicationField('displayName', e.target.value); this.updateApplicationField("displayName", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} : {Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} :
</Col> </Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: '100%'} :{}}> <Col span={22} style={(Setting.isMobile()) ? {maxWidth: "100%"} :{}}>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
<Input prefix={<LinkOutlined />} value={this.state.application.logo} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.application.logo} onChange={e => {
this.updateApplicationField('logo', e.target.value); this.updateApplicationField("logo", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.application.logo}> <a target="_blank" rel="noreferrer" href={this.state.application.logo}>
<img src={this.state.application.logo} alt={this.state.application.logo} height={90} style={{marginBottom: '20px'}}/> <img src={this.state.application.logo} alt={this.state.application.logo} height={90} style={{marginBottom: "20px"}} />
</a> </a>
</Col> </Col>
</Row> </Row>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Home"), i18next.t("general:Home - Tooltip"))} : {Setting.getLabel(i18next.t("general:Home"), i18next.t("general:Home - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.homepageUrl} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.application.homepageUrl} onChange={e => {
this.updateApplicationField('homepageUrl', e.target.value); this.updateApplicationField("homepageUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} : {Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.application.description} onChange={e => { <Input value={this.state.application.description} onChange={e => {
this.updateApplicationField('description', e.target.value); this.updateApplicationField("description", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.application.organization} onChange={(value => {this.updateApplicationField('organization', value);})}> <Select virtual={false} style={{width: "100%"}} value={this.state.application.organization} onChange={(value => {this.updateApplicationField("organization", value);})}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.application.clientId} onChange={e => { <Input value={this.state.application.clientId} onChange={e => {
this.updateApplicationField('clientId', e.target.value); this.updateApplicationField("clientId", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.application.clientSecret} onChange={e => { <Input value={this.state.application.clientSecret} onChange={e => {
this.updateApplicationField('clientSecret', e.target.value); this.updateApplicationField("clientSecret", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} : {Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.application.cert} onChange={(value => {this.updateApplicationField('cert', value);})}> <Select virtual={false} style={{width: "100%"}} value={this.state.application.cert} onChange={(value => {this.updateApplicationField("cert", value);})}>
{ {
this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>) this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Redirect URLs"), i18next.t("application:Redirect URLs - Tooltip"))} : {Setting.getLabel(i18next.t("application:Redirect URLs"), i18next.t("application:Redirect URLs - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<UrlTable <UrlTable
title={i18next.t("application:Redirect URLs")} title={i18next.t("application:Redirect URLs")}
table={this.state.application.redirectUris} table={this.state.application.redirectUris}
onUpdateTable={(value) => { this.updateApplicationField('redirectUris', value)}} onUpdateTable={(value) => {this.updateApplicationField("redirectUris", value);}}
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} : {Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.application.tokenFormat} onChange={(value => {this.updateApplicationField('tokenFormat', value);})}> <Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenFormat} onChange={(value => {this.updateApplicationField("tokenFormat", value);})}>
{ {
['JWT', 'JWT-Empty'] ["JWT", "JWT-Empty"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>) .map((item, index) => <Option key={index} value={item}>{item}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} : {Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input style={{width: "150px"}} value={this.state.application.expireInHours} suffix="Hours" onChange={e => { <Input style={{width: "150px"}} value={this.state.application.expireInHours} suffix="Hours" onChange={e => {
this.updateApplicationField('expireInHours', e.target.value); this.updateApplicationField("expireInHours", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Refresh token expire"), i18next.t("application:Refresh token expire - Tooltip"))} : {Setting.getLabel(i18next.t("application:Refresh token expire"), i18next.t("application:Refresh token expire - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input style={{width: "150px"}} value={this.state.application.refreshExpireInHours} suffix="Hours" onChange={e => { <Input style={{width: "150px"}} value={this.state.application.refreshExpireInHours} suffix="Hours" onChange={e => {
this.updateApplicationField('refreshExpireInHours', e.target.value); this.updateApplicationField("refreshExpireInHours", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Password ON"), i18next.t("application:Password ON - Tooltip"))} : {Setting.getLabel(i18next.t("application:Password ON"), i18next.t("application:Password ON - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.application.enablePassword} onChange={checked => { <Switch checked={this.state.application.enablePassword} onChange={checked => {
this.updateApplicationField('enablePassword', checked); this.updateApplicationField("enablePassword", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} : {Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.application.enableSignUp} onChange={checked => { <Switch checked={this.state.application.enableSignUp} onChange={checked => {
this.updateApplicationField('enableSignUp', checked); this.updateApplicationField("enableSignUp", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Signin session"), i18next.t("application:Enable signin session - Tooltip"))} : {Setting.getLabel(i18next.t("application:Signin session"), i18next.t("application:Enable signin session - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.application.enableSigninSession} onChange={checked => { <Switch checked={this.state.application.enableSigninSession} onChange={checked => {
this.updateApplicationField('enableSigninSession', checked); this.updateApplicationField("enableSigninSession", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable code signin"), i18next.t("application:Enable code signin - Tooltip"))} : {Setting.getLabel(i18next.t("application:Enable code signin"), i18next.t("application:Enable code signin - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.application.enableCodeSignin} onChange={checked => { <Switch checked={this.state.application.enableCodeSignin} onChange={checked => {
this.updateApplicationField('enableCodeSignin', checked); this.updateApplicationField("enableCodeSignin", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable WebAuthn signin"), i18next.t("application:Enable WebAuthn signin - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.application.enableWebAuthn} onChange={checked => {
this.updateApplicationField("enableWebAuthn", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.signupUrl} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.application.signupUrl} onChange={e => {
this.updateApplicationField('signupUrl', e.target.value); this.updateApplicationField("signupUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Signin URL"), i18next.t("general:Signin URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:Signin URL"), i18next.t("general:Signin URL - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.signinUrl} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.application.signinUrl} onChange={e => {
this.updateApplicationField('signinUrl', e.target.value); this.updateApplicationField("signinUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Forget URL"), i18next.t("general:Forget URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:Forget URL"), i18next.t("general:Forget URL - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.forgetUrl} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.application.forgetUrl} onChange={e => {
this.updateApplicationField('forgetUrl', e.target.value); this.updateApplicationField("forgetUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Affiliation URL"), i18next.t("general:Affiliation URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:Affiliation URL"), i18next.t("general:Affiliation URL - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.affiliationUrl} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.application.affiliationUrl} onChange={e => {
this.updateApplicationField('affiliationUrl', e.target.value); this.updateApplicationField("affiliationUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Terms of Use"), i18next.t("provider:Terms of Use - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Terms of Use"), i18next.t("provider:Terms of Use - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -402,13 +412,13 @@ class ApplicationEditPage extends React.Component {
this.updateApplicationField("termsOfUse", e.target.value); this.updateApplicationField("termsOfUse", e.target.value);
}} /> }} />
<Upload maxCount={1} accept=".html" showUploadList={false} <Upload maxCount={1} accept=".html" showUploadList={false}
beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}> beforeUpload={file => {return false;}} onChange={info => {this.handleUpload(info);}}>
<Button icon={<UploadOutlined />} loading={this.state.uploading}>{i18next.t("general:Click to Upload")}</Button> <Button icon={<UploadOutlined />} loading={this.state.uploading}>{i18next.t("general:Click to Upload")}</Button>
</Upload> </Upload>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Signup HTML"), i18next.t("provider:Signup HTML - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Signup HTML"), i18next.t("provider:Signup HTML - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -416,7 +426,7 @@ class ApplicationEditPage extends React.Component {
<div style={{width: "900px", height: "300px"}} > <div style={{width: "900px", height: "300px"}} >
<CodeMirror <CodeMirror
value={this.state.application.signupHtml} value={this.state.application.signupHtml}
options={{mode: 'htmlmixed', theme: "material-darker"}} options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => { onBeforeChange={(editor, data, value) => {
this.updateApplicationField("signupHtml", value); this.updateApplicationField("signupHtml", value);
}} }}
@ -424,13 +434,13 @@ class ApplicationEditPage extends React.Component {
</div> </div>
} title={i18next.t("provider:Signup HTML - Edit")} trigger="click"> } title={i18next.t("provider:Signup HTML - Edit")} trigger="click">
<Input value={this.state.application.signupHtml} style={{marginBottom: "10px"}} onChange={e => { <Input value={this.state.application.signupHtml} style={{marginBottom: "10px"}} onChange={e => {
this.updateApplicationField("signupHtml", e.target.value) this.updateApplicationField("signupHtml", e.target.value);
}} /> }} />
</Popover> </Popover>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Signin HTML"), i18next.t("provider:Signin HTML - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Signin HTML"), i18next.t("provider:Signin HTML - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -438,7 +448,7 @@ class ApplicationEditPage extends React.Component {
<div style={{width: "900px", height: "300px"}} > <div style={{width: "900px", height: "300px"}} >
<CodeMirror <CodeMirror
value={this.state.application.signinHtml} value={this.state.application.signinHtml}
options={{mode: 'htmlmixed', theme: "material-darker"}} options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => { onBeforeChange={(editor, data, value) => {
this.updateApplicationField("signinHtml", value); this.updateApplicationField("signinHtml", value);
}} }}
@ -446,20 +456,20 @@ class ApplicationEditPage extends React.Component {
</div> </div>
} title={i18next.t("provider:Signin HTML - Edit")} trigger="click"> } title={i18next.t("provider:Signin HTML - Edit")} trigger="click">
<Input value={this.state.application.signinHtml} style={{marginBottom: "10px"}} onChange={e => { <Input value={this.state.application.signinHtml} style={{marginBottom: "10px"}} onChange={e => {
this.updateApplicationField("signinHtml", e.target.value) this.updateApplicationField("signinHtml", e.target.value);
}} /> }} />
</Popover> </Popover>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} : {Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} <Select virtual={false} mode="tags" style={{width: "100%"}}
value={this.state.application.grantTypes} value={this.state.application.grantTypes}
onChange={(value => { onChange={(value => {
this.updateApplicationField('grantTypes', value); this.updateApplicationField("grantTypes", value);
})} > })} >
{ {
[ [
@ -474,20 +484,38 @@ class ApplicationEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 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"))} : {Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
</Col> </Col>
<Col span={22}> <Col span={22}>
<CodeMirror <CodeMirror
value={this.state.samlMetadata} value={this.state.samlMetadata}
options={{mode: 'xml', theme: 'default'}} options={{mode: "xml", theme: "default"}}
onBeforeChange={(editor, data, value) => {}} 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> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} : {Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -496,12 +524,12 @@ class ApplicationEditPage extends React.Component {
table={this.state.application.providers} table={this.state.application.providers}
providers={this.state.providers} providers={this.state.providers}
application={this.state.application} application={this.state.application}
onUpdateTable={(value) => { this.updateApplicationField('providers', value)}} onUpdateTable={(value) => {this.updateApplicationField("providers", value);}}
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col> </Col>
{ {
@ -510,22 +538,22 @@ class ApplicationEditPage extends React.Component {
</Row> </Row>
{ {
!this.state.application.enableSignUp ? null : ( !this.state.application.enableSignUp ? null : (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Signup items"), i18next.t("application:Signup items - Tooltip"))} : {Setting.getLabel(i18next.t("application:Signup items"), i18next.t("application:Signup items - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<SignupTable <SignupTable
title={i18next.t("application:Signup items")} title={i18next.t("application:Signup items")}
table={this.state.application.signupItems} table={this.state.application.signupItems}
onUpdateTable={(value) => { this.updateApplicationField('signupItems', value)}} onUpdateTable={(value) => {this.updateApplicationField("signupItems", value);}}
/> />
</Col> </Col>
</Row> </Row>
) )
} }
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col> </Col>
{ {
@ -533,13 +561,13 @@ class ApplicationEditPage extends React.Component {
} }
</Row> </Row>
</Card> </Card>
) );
} }
renderSignupSigninPreview() { renderSignupSigninPreview() {
let signUpUrl = `/signup/${this.state.application.name}`; let signUpUrl = `/signup/${this.state.application.name}`;
let signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`; let signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`;
let maskStyle = {position: 'absolute', top: '0px', left: '0px', zIndex: 10, height: '100%', width: '100%', background: 'rgba(0,0,0,0.4)'}; let maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"};
if (!this.state.application.enablePassword) { if (!this.state.application.enablePassword) {
signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize"); signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize");
} }
@ -581,12 +609,12 @@ class ApplicationEditPage extends React.Component {
</div> </div>
</Col> </Col>
</React.Fragment> </React.Fragment>
) );
} }
renderPromptPreview() { renderPromptPreview() {
let promptUrl = `/prompt/${this.state.application.name}`; let promptUrl = `/prompt/${this.state.application.name}`;
let maskStyle = {position: 'absolute', top: '0px', left: '0px', zIndex: 10, height: '100%', width: '100%', background: 'rgba(0,0,0,0.4)'}; let maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"};
return ( return (
<Col span={11}> <Col span={11}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => { <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
@ -602,7 +630,7 @@ class ApplicationEditPage extends React.Component {
<div style={maskStyle}></div> <div style={maskStyle}></div>
</div> </div>
</Col> </Col>
) );
} }
submitApplicationEdit(willExist) { submitApplicationEdit(willExist) {
@ -610,19 +638,19 @@ class ApplicationEditPage extends React.Component {
ApplicationBackend.updateApplication(this.state.application.owner, this.state.applicationName, application) ApplicationBackend.updateApplication(this.state.application.owner, this.state.applicationName, application)
.then((res) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", "Successfully saved");
this.setState({ this.setState({
applicationName: this.state.application.name, applicationName: this.state.application.name,
}); });
if (willExist) { if (willExist) {
this.props.history.push(`/applications`); this.props.history.push("/applications");
} else { } else {
this.props.history.push(`/applications/${this.state.application.name}`); this.props.history.push(`/applications/${this.state.application.name}`);
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
this.updateApplicationField('name', this.state.applicationName); this.updateApplicationField("name", this.state.applicationName);
} }
}) })
.catch(error => { .catch(error => {
@ -633,7 +661,7 @@ class ApplicationEditPage extends React.Component {
deleteApplication() { deleteApplication() {
ApplicationBackend.deleteApplication(this.state.application) ApplicationBackend.deleteApplication(this.state.application)
.then(() => { .then(() => {
this.props.history.push(`/applications`); this.props.history.push("/applications");
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Application failed to delete: ${error}`); Setting.showMessage("error", `Application failed to delete: ${error}`);
@ -646,10 +674,10 @@ class ApplicationEditPage extends React.Component {
{ {
this.state.application !== null ? this.renderApplication() : null this.state.application !== null ? this.renderApplication() : null
} }
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from 'antd'; import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from "antd";
import {EditOutlined} from "@ant-design/icons"; import {EditOutlined} from "@ant-design/icons";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -23,7 +23,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class ApplicationListPage extends BaseListPage { class ApplicationListPage extends BaseListPage {
newApplication() { newApplication() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {
@ -36,7 +35,10 @@ class ApplicationListPage extends BaseListPage {
enableSignUp: true, enableSignUp: true,
enableSigninSession: false, enableSigninSession: false,
enableCodeSignin: false, enableCodeSignin: false,
providers: [], enableSamlCompress: false,
providers: [
{name: "provider_captcha_default", canSignUp: false, canSignIn: false, canUnlink: false, prompted: false, alertType: "None"},
],
signupItems: [ signupItems: [
{name: "ID", visible: false, required: true, rule: "Random"}, {name: "ID", visible: false, required: true, rule: "Random"},
{name: "Username", visible: true, required: true, rule: "None"}, {name: "Username", visible: true, required: true, rule: "None"},
@ -51,7 +53,7 @@ class ApplicationListPage extends BaseListPage {
redirectUris: ["http://localhost:9000/callback"], redirectUris: ["http://localhost:9000/callback"],
tokenFormat: "JWT", tokenFormat: "JWT",
expireInHours: 24 * 7, expireInHours: 24 * 7,
} };
} }
addApplication() { addApplication() {
@ -69,7 +71,7 @@ class ApplicationListPage extends BaseListPage {
deleteApplication(i) { deleteApplication(i) {
ApplicationBackend.deleteApplication(this.state.data[i]) ApplicationBackend.deleteApplication(this.state.data[i])
.then((res) => { .then((res) => {
Setting.showMessage("success", `Application deleted successfully`); Setting.showMessage("success", "Application deleted successfully");
this.setState({ this.setState({
data: Setting.deleteRow(this.state.data, i), data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1}, pagination: {total: this.state.pagination.total - 1},
@ -85,25 +87,25 @@ class ApplicationListPage extends BaseListPage {
const columns = [ const columns = [
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
width: '150px', width: "150px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/applications/${text}`}> <Link to={`/applications/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: "createdTime",
key: 'createdTime', key: "createdTime",
width: '160px', width: "160px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -111,45 +113,45 @@ class ApplicationListPage extends BaseListPage {
}, },
{ {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: 'displayName', dataIndex: "displayName",
key: 'displayName', key: "displayName",
// width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('displayName'), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: 'Logo', title: "Logo",
dataIndex: 'logo', dataIndex: "logo",
key: 'logo', key: "logo",
width: '200px', width: "200px",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<a target="_blank" rel="noreferrer" href={text}> <a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={150} /> <img src={text} alt={text} width={150} />
</a> </a>
) );
} }
}, },
{ {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
dataIndex: 'organization', dataIndex: "organization",
key: 'organization', key: "organization",
width: '150px', width: "150px",
sorter: true, sorter: true,
...this.getColumnSearchProps('organization'), ...this.getColumnSearchProps("organization"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/organizations/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Providers"), title: i18next.t("general:Providers"),
dataIndex: 'providers', dataIndex: "providers",
key: 'providers', key: "providers",
...this.getColumnSearchProps('providers'), ...this.getColumnSearchProps("providers"),
// width: '600px', // width: '600px',
render: (text, record, index) => { render: (text, record, index) => {
const providers = text; const providers = text;
@ -177,11 +179,11 @@ class ApplicationListPage extends BaseListPage {
</Link> </Link>
</div> </div>
</List.Item> </List.Item>
) );
}} }}
/> />
) );
} };
return ( return (
<div> <div>
@ -198,28 +200,28 @@ class ApplicationListPage extends BaseListPage {
</Col> </Col>
</Row> </Row>
</div> </div>
) );
}, },
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: "",
key: 'op', key: "op",
width: '170px', width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/applications/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/applications/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm <Popconfirm
title={`Sure to delete application: ${record.name} ?`} title={`Sure to delete application: ${record.name} ?`}
onConfirm={() => this.deleteApplication(index)} onConfirm={() => this.deleteApplication(index)}
disabled={record.name === "app-built-in"} disabled={record.name === "app-built-in"}
> >
<Button style={{marginBottom: '10px'}} disabled={record.name === "app-built-in"} type="danger">{i18next.t("general:Delete")}</Button> <Button style={{marginBottom: "10px"}} disabled={record.name === "app-built-in"} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -233,7 +235,7 @@ class ApplicationListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={applications} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={applications} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Applications")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Applications")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -28,8 +28,9 @@ class BaseListPage extends React.Component {
pageSize: 10, pageSize: 10,
}, },
loading: false, loading: false,
searchText: '', searchText: "",
searchedColumn: '', searchedColumn: "",
isAuthorized: true,
}; };
} }
@ -49,7 +50,7 @@ class BaseListPage extends React.Component {
value={selectedKeys[0]} value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)} onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{ marginBottom: 8, display: 'block' }} style={{marginBottom: 8, display: "block"}}
/> />
<Space> <Space>
<Button <Button
@ -80,11 +81,11 @@ class BaseListPage extends React.Component {
</Space> </Space>
</div> </div>
), ),
filterIcon: filtered => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />, filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1890ff" : undefined}} />,
onFilter: (value, record) => onFilter: (value, record) =>
record[dataIndex] record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()) ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: '', : "",
onFilterDropdownVisibleChange: visible => { onFilterDropdownVisibleChange: visible => {
if (visible) { if (visible) {
setTimeout(() => this.searchInput.select(), 100); setTimeout(() => this.searchInput.select(), 100);
@ -93,10 +94,10 @@ class BaseListPage extends React.Component {
render: text => render: text =>
this.state.searchedColumn === dataIndex ? ( this.state.searchedColumn === dataIndex ? (
<Highlighter <Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }} highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
searchWords={[this.state.searchText]} searchWords={[this.state.searchText]}
autoEscape autoEscape
textToHighlight={text ? text.toString() : ''} textToHighlight={text ? text.toString() : ""}
/> />
) : ( ) : (
text text

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd'; import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
@ -70,127 +70,127 @@ class CertEditPage extends React.Component {
<div> <div>
{this.state.mode === "add" ? i18next.t("cert:New Cert") : i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("cert:New Cert") : i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> } style={(Setting.isMobile())? {margin: "5px"}:{}} type="inner">
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: "10px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.cert.name} onChange={e => { <Input value={this.state.cert.name} onChange={e => {
this.updateCertField('name', e.target.value); this.updateCertField("name", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.cert.displayName} onChange={e => { <Input value={this.state.cert.displayName} onChange={e => {
this.updateCertField('displayName', e.target.value); this.updateCertField("displayName", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Scope"), i18next.t("cert:Scope - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Scope"), i18next.t("cert:Scope - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.scope} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.cert.scope} onChange={(value => {
this.updateCertField('scope', value); this.updateCertField("scope", value);
})}> })}>
{ {
[ [
{id: 'JWT', name: 'JWT'}, {id: "JWT", name: "JWT"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Type"), i18next.t("cert:Type - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.type} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.cert.type} onChange={(value => {
this.updateCertField('type', value); this.updateCertField("type", value);
})}> })}>
{ {
[ [
{id: 'x509', name: 'x509'}, {id: "x509", name: "x509"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Crypto algorithm"), i18next.t("cert:Crypto algorithm - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Crypto algorithm"), i18next.t("cert:Crypto algorithm - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.cert.cryptoAlgorithm} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.cert.cryptoAlgorithm} onChange={(value => {
this.updateCertField('cryptoAlgorithm', value); this.updateCertField("cryptoAlgorithm", value);
})}> })}>
{ {
[ [
{id: 'RS256', name: 'RS256'}, {id: "RS256", name: "RS256"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.cert.bitSize} onChange={value => { <InputNumber value={this.state.cert.bitSize} onChange={value => {
this.updateCertField('bitSize', value); this.updateCertField("bitSize", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.cert.expireInYears} onChange={value => { <InputNumber value={this.state.cert.expireInYears} onChange={value => {
this.updateCertField('expireInYears', value); this.updateCertField("expireInYears", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Public key"), i18next.t("cert:Public key - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Certificate"), i18next.t("cert:Certificate - Tooltip"))} :
</Col> </Col>
<Col span={9} > <Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => { <Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
copy(this.state.cert.publicKey); copy(this.state.cert.certificate);
Setting.showMessage("success", i18next.t("cert:Public key copied to clipboard successfully")); Setting.showMessage("success", i18next.t("cert:Certificate copied to clipboard successfully"));
}} }}
> >
{i18next.t("cert:Copy public key")} {i18next.t("cert:Copy certificate")}
</Button> </Button>
<Button type="primary" onClick={() => { <Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.publicKey], {type: "text/plain;charset=utf-8"}); const blob = new Blob([this.state.cert.certificate], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.pem"); FileSaver.saveAs(blob, "token_jwt_key.pem");
}} }}
> >
{i18next.t("cert:Download public key")} {i18next.t("cert:Download certificate")}
</Button> </Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.publicKey} onChange={e => { <TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.certificate} onChange={e => {
this.updateCertField('publicKey', e.target.value); this.updateCertField("certificate", e.target.value);
}} /> }} />
</Col> </Col>
<Col span={1} /> <Col span={1} />
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Private key"), i18next.t("cert:Private key - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Private key"), i18next.t("cert:Private key - Tooltip"))} :
</Col> </Col>
<Col span={9} > <Col span={9} >
<Button style={{marginRight: '10px', marginBottom: '10px'}} onClick={() => { <Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
copy(this.state.cert.privateKey); copy(this.state.cert.privateKey);
Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully")); Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully"));
}} }}
@ -205,12 +205,12 @@ class CertEditPage extends React.Component {
{i18next.t("cert:Download private key")} {i18next.t("cert:Download private key")}
</Button> </Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.privateKey} onChange={e => { <TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.privateKey} onChange={e => {
this.updateCertField('privateKey', e.target.value); this.updateCertField("privateKey", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
</Card> </Card>
) );
} }
submitCertEdit(willExist) { submitCertEdit(willExist) {
@ -218,19 +218,19 @@ class CertEditPage extends React.Component {
CertBackend.updateCert(this.state.cert.owner, this.state.certName, cert) CertBackend.updateCert(this.state.cert.owner, this.state.certName, cert)
.then((res) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", "Successfully saved");
this.setState({ this.setState({
certName: this.state.cert.name, certName: this.state.cert.name,
}); });
if (willExist) { if (willExist) {
this.props.history.push(`/certs`); this.props.history.push("/certs");
} else { } else {
this.props.history.push(`/certs/${this.state.cert.name}`); this.props.history.push(`/certs/${this.state.cert.name}`);
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
this.updateCertField('name', this.state.certName); this.updateCertField("name", this.state.certName);
} }
}) })
.catch(error => { .catch(error => {
@ -241,7 +241,7 @@ class CertEditPage extends React.Component {
deleteCert() { deleteCert() {
CertBackend.deleteCert(this.state.cert) CertBackend.deleteCert(this.state.cert)
.then(() => { .then(() => {
this.props.history.push(`/certs`); this.props.history.push("/certs");
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`); Setting.showMessage("error", `Cert failed to delete: ${error}`);
@ -254,10 +254,10 @@ class CertEditPage extends React.Component {
{ {
this.state.cert !== null ? this.renderCert() : null this.state.cert !== null ? this.renderCert() : null
} }
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Popconfirm, Table} from 'antd'; import {Button, Popconfirm, Table} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
@ -22,7 +22,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class CertListPage extends BaseListPage { class CertListPage extends BaseListPage {
newCert() { newCert() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {
@ -35,9 +34,9 @@ class CertListPage extends BaseListPage {
cryptoAlgorithm: "RS256", cryptoAlgorithm: "RS256",
bitSize: 4096, bitSize: 4096,
expireInYears: 20, expireInYears: 20,
publicKey: "", certificate: "",
privateKey: "", privateKey: "",
} };
} }
addCert() { addCert() {
@ -55,7 +54,7 @@ class CertListPage extends BaseListPage {
deleteCert(i) { deleteCert(i) {
CertBackend.deleteCert(this.state.data[i]) CertBackend.deleteCert(this.state.data[i])
.then((res) => { .then((res) => {
Setting.showMessage("success", `Cert deleted successfully`); Setting.showMessage("success", "Cert deleted successfully");
this.setState({ this.setState({
data: Setting.deleteRow(this.state.data, i), data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1}, pagination: {total: this.state.pagination.total - 1},
@ -71,25 +70,25 @@ class CertListPage extends BaseListPage {
const columns = [ const columns = [
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
width: '120px', width: "120px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/certs/${text}`}> <Link to={`/certs/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: "createdTime",
key: 'createdTime', key: "createdTime",
width: '180px', width: "180px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -97,79 +96,79 @@ class CertListPage extends BaseListPage {
}, },
{ {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: 'displayName', dataIndex: "displayName",
key: 'displayName', key: "displayName",
// width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('displayName'), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: i18next.t("cert:Scope"), title: i18next.t("cert:Scope"),
dataIndex: 'scope', dataIndex: "scope",
key: 'scope', key: "scope",
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: 'JWT', value: 'JWT'}, {text: "JWT", value: "JWT"},
], ],
width: '110px', width: "110px",
sorter: true, sorter: true,
}, },
{ {
title: i18next.t("cert:Type"), title: i18next.t("cert:Type"),
dataIndex: 'type', dataIndex: "type",
key: 'type', key: "type",
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: 'x509', value: 'x509'}, {text: "x509", value: "x509"},
], ],
width: '110px', width: "110px",
sorter: true, sorter: true,
}, },
{ {
title: i18next.t("cert:Crypto algorithm"), title: i18next.t("cert:Crypto algorithm"),
dataIndex: 'cryptoAlgorithm', dataIndex: "cryptoAlgorithm",
key: 'cryptoAlgorithm', key: "cryptoAlgorithm",
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: 'RS256', value: 'RS256'}, {text: "RS256", value: "RS256"},
], ],
width: '190px', width: "190px",
sorter: true, sorter: true,
}, },
{ {
title: i18next.t("cert:Bit size"), title: i18next.t("cert:Bit size"),
dataIndex: 'bitSize', dataIndex: "bitSize",
key: 'bitSize', key: "bitSize",
width: '130px', width: "130px",
sorter: true, sorter: true,
...this.getColumnSearchProps('bitSize'), ...this.getColumnSearchProps("bitSize"),
}, },
{ {
title: i18next.t("cert:Expire in years"), title: i18next.t("cert:Expire in years"),
dataIndex: 'expireInYears', dataIndex: "expireInYears",
key: 'expireInYears', key: "expireInYears",
width: '170px', width: "170px",
sorter: true, sorter: true,
...this.getColumnSearchProps('expireInYears'), ...this.getColumnSearchProps("expireInYears"),
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: "",
key: 'op', key: "op",
width: '170px', width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/certs/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/certs/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm <Popconfirm
title={`Sure to delete cert: ${record.name} ?`} title={`Sure to delete cert: ${record.name} ?`}
onConfirm={() => this.deleteCert(index)} onConfirm={() => this.deleteCert(index)}
> >
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button> <Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -183,7 +182,7 @@ class CertListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={certs} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={certs} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -16,7 +16,7 @@ import React, {useState} from "react";
import Cropper from "react-cropper"; import Cropper from "react-cropper";
import "cropperjs/dist/cropper.css"; import "cropperjs/dist/cropper.css";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {Button, Row, Col, Modal} from 'antd'; import {Button, Col, Modal, Row} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import * as ResourceBackend from "./backend/ResourceBackend"; import * as ResourceBackend from "./backend/ResourceBackend";
@ -55,7 +55,7 @@ export const CropperDiv = (props) => {
return false; return false;
} }
// Setting.showMessage("success", "uploading..."); // Setting.showMessage("success", "uploading...");
const extension = image.substring(image.indexOf('/') + 1, image.indexOf(';base64')); const extension = image.substring(image.indexOf("/") + 1, image.indexOf(";base64"));
const fullFilePath = `avatar/${user.owner}/${user.name}.${extension}`; const fullFilePath = `avatar/${user.owner}/${user.name}.${extension}`;
ResourceBackend.uploadResource(user.owner, user.name, "avatar", "CropperDiv", fullFilePath, blob) ResourceBackend.uploadResource(user.owner, user.name, "avatar", "CropperDiv", fullFilePath, blob)
.then((res) => { .then((res) => {
@ -67,7 +67,7 @@ export const CropperDiv = (props) => {
}); });
return true; return true;
}); });
} };
const showModal = () => { const showModal = () => {
setVisible(true); setVisible(true);
@ -81,13 +81,13 @@ export const CropperDiv = (props) => {
}; };
const handleCancel = () => { const handleCancel = () => {
console.log('Clicked cancel button'); console.log("Clicked cancel button");
setVisible(false); setVisible(false);
}; };
const selectFile = () => { const selectFile = () => {
uploadButton.click(); uploadButton.click();
} };
return ( return (
<div> <div>
@ -131,7 +131,7 @@ export const CropperDiv = (props) => {
</Col> </Col>
</Modal> </Modal>
</div> </div>
) );
} };
export default CropperDiv; export default CropperDiv;

View File

@ -43,11 +43,11 @@ class LdapEditPage extends React.Component {
if (res.status === "ok") { if (res.status === "ok") {
this.setState({ this.setState({
ldap: res.data ldap: res.data
}) });
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
} }
}) });
} }
getOrganizations() { getOrganizations() {
@ -73,7 +73,7 @@ class LdapEditPage extends React.Component {
color: "#faad14", color: "#faad14",
marginLeft: "20px" marginLeft: "20px"
}}>{i18next.t("ldap:The Auto Sync option will sync all users to specify organization")}</span> }}>{i18next.t("ldap:The Auto Sync option will sync all users to specify organization")}</span>
) );
} }
} }
@ -186,17 +186,17 @@ class LdapEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
</Card> </Card>
) );
} }
submitLdapEdit() { submitLdapEdit() {
LddpBackend.updateLdap(this.state.ldap) LddpBackend.updateLdap(this.state.ldap)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", `Update LDAP server success`); Setting.showMessage("success", "Update LDAP server success");
this.setState((prevState) => { this.setState((prevState) => {
prevState.ldap = res.data2; prevState.ldap = res.data2;
}) });
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
} }

View File

@ -28,7 +28,7 @@ class LdapListPage extends React.Component {
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getLdaps() this.getLdaps();
} }
getLdaps() { getLdaps() {
@ -43,7 +43,7 @@ class LdapListPage extends React.Component {
this.setState((prevState) => { this.setState((prevState) => {
prevState.ldaps = ldapsData; prevState.ldaps = ldapsData;
return prevState; return prevState;
}) });
}); });
} }
@ -64,7 +64,7 @@ class LdapListPage extends React.Component {
<Link to={`/ldaps/${record.id}`}> <Link to={`/ldaps/${record.id}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
@ -78,7 +78,7 @@ class LdapListPage extends React.Component {
<Link to={`/organizations/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
@ -88,7 +88,7 @@ class LdapListPage extends React.Component {
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.host.localeCompare(b.host), sorter: (a, b) => a.host.localeCompare(b.host),
render: (text, record, index) => { render: (text, record, index) => {
return `${text}:${record.port}` return `${text}:${record.port}`;
} }
}, },
{ {
@ -113,7 +113,7 @@ class LdapListPage extends React.Component {
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync), sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
render: (text, record, index) => { render: (text, record, index) => {
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : ( return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
<span style={{color: "#52c41a"}}>{text + " mins"}</span>) <span style={{color: "#52c41a"}}>{text + " mins"}</span>);
} }
}, },
{ {
@ -123,7 +123,7 @@ class LdapListPage extends React.Component {
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync), sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
render: (text, record, index) => { render: (text, record, index) => {
return text return text;
} }
}, },
{ {
@ -147,7 +147,7 @@ class LdapListPage extends React.Component {
type="danger">{i18next.t("general:Delete")}</Button> type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -161,7 +161,7 @@ class LdapListPage extends React.Component {
<span>{i18next.t("general:LDAPs")}</span> <span>{i18next.t("general:LDAPs")}</span>
<Button type="primary" size="small" style={{marginLeft: "10px"}} <Button type="primary" size="small" style={{marginLeft: "10px"}}
onClick={() => { onClick={() => {
this.addLdap() this.addLdap();
}}>{i18next.t("general:Add")}</Button> }}>{i18next.t("general:Add")}</Button>
</div> </div>
)} )}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Col, Row, Table, Popconfirm} from "antd"; import {Button, Col, Popconfirm, Row, Table} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend"; import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next"; import i18next from "i18next";
@ -31,14 +31,14 @@ class LdapSyncPage extends React.Component {
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getLdap() this.getLdap();
} }
syncUsers() { syncUsers() {
let selectedUsers = this.state.selectedUsers; let selectedUsers = this.state.selectedUsers;
if (selectedUsers === null || selectedUsers.length === 0) { if (selectedUsers === null || selectedUsers.length === 0) {
Setting.showMessage("error", "Please select al least 1 user first"); Setting.showMessage("error", "Please select al least 1 user first");
return return;
} }
LdapBackend.syncUsers(this.state.ldap.owner, this.state.ldap.id, selectedUsers) LdapBackend.syncUsers(this.state.ldap.owner, this.state.ldap.id, selectedUsers)
@ -62,14 +62,14 @@ class LdapSyncPage extends React.Component {
if (failed && failed.length > 0) { if (failed && failed.length > 0) {
failed.forEach(elem => { failed.forEach(elem => {
failedUser.push(elem.cn); failedUser.push(elem.cn);
}) });
Setting.showMessage("error", `Sync [${failedUser}] failed`) Setting.showMessage("error", `Sync [${failedUser}] failed`);
} }
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
} }
})) }));
} }
getLdap() { getLdap() {
@ -79,7 +79,7 @@ class LdapSyncPage extends React.Component {
this.setState((prevState) => { this.setState((prevState) => {
prevState.ldap = res.data; prevState.ldap = res.data;
return prevState; return prevState;
}) });
this.getLdapUser(res.data); this.getLdapUser(res.data);
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
@ -95,28 +95,28 @@ class LdapSyncPage extends React.Component {
this.setState((prevState) => { this.setState((prevState) => {
prevState.users = res.data.users; prevState.users = res.data.users;
return prevState; return prevState;
}) });
this.getExistUsers(ldap.owner, res.data.users); this.getExistUsers(ldap.owner, res.data.users);
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
} }
}) });
} }
getExistUsers(owner, users) { getExistUsers(owner, users) {
let uuidArray = []; let uuidArray = [];
users.forEach(elem => { users.forEach(elem => {
uuidArray.push(elem.uuid); uuidArray.push(elem.uuid);
}) });
LdapBackend.checkLdapUsersExist(owner, uuidArray) LdapBackend.checkLdapUsersExist(owner, uuidArray)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.setState(prevState => { this.setState(prevState => {
prevState.existUuids = res.data?.length > 0 ? res.data : []; prevState.existUuids = res.data?.length > 0 ? res.data : [];
return prevState; return prevState;
}) });
} }
}) });
} }
buildValArray(data, key) { buildValArray(data, key) {
@ -137,7 +137,7 @@ class LdapSyncPage extends React.Component {
let filterArray = []; let filterArray = [];
if (data !== null && data.length > 0) { if (data !== null && data.length > 0) {
let valArray = this.buildValArray(data, key) let valArray = this.buildValArray(data, key);
valArray.forEach(elem => { valArray.forEach(elem => {
filterArray.push({ filterArray.push({
text: elem, text: elem,
@ -163,7 +163,7 @@ class LdapSyncPage extends React.Component {
width: "200px", width: "200px",
sorter: (a, b) => a.uidNumber.localeCompare(b.uidNumber), sorter: (a, b) => a.uidNumber.localeCompare(b.uidNumber),
render: (text, record, index) => { render: (text, record, index) => {
return `${text} / ${record.uid}` return `${text} / ${record.uid}`;
}, },
}, },
{ {
@ -202,7 +202,7 @@ class LdapSyncPage extends React.Component {
this.setState(prevState => { this.setState(prevState => {
prevState.selectedUsers = selectedRows; prevState.selectedUsers = selectedRows;
return prevState; return prevState;
}) });
}, },
getCheckboxProps: record => ({ getCheckboxProps: record => ({
disabled: this.state.existUuids.indexOf(record.uuid) !== -1, disabled: this.state.existUuids.indexOf(record.uuid) !== -1,
@ -217,7 +217,7 @@ class LdapSyncPage extends React.Component {
<div> <div>
<span>{this.state.ldap?.serverName}</span> <span>{this.state.ldap?.serverName}</span>
<Popconfirm placement={"right"} <Popconfirm placement={"right"}
title={`Please confirm to sync selected users`} title={"Please confirm to sync selected users"}
onConfirm={() => this.syncUsers()} onConfirm={() => this.syncUsers()}
> >
<Button type="primary" size="small" <Button type="primary" size="small"

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Col, Popconfirm, Row, Table} from 'antd'; import {Button, Col, Popconfirm, Row, Table} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as LdapBackend from "./backend/LdapBackend"; import * as LdapBackend from "./backend/LdapBackend";
@ -49,7 +49,7 @@ class LdapTable extends React.Component {
baseDn: "ou=People,dc=example,dc=com", baseDn: "ou=People,dc=example,dc=com",
autosync: 0, autosync: 0,
lastSync: "" lastSync: ""
} };
} }
addRow(table) { addRow(table) {
@ -57,7 +57,7 @@ class LdapTable extends React.Component {
LdapBackend.addLdap(newLdap) LdapBackend.addLdap(newLdap)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", `Add LDAP server success`); Setting.showMessage("success", "Add LDAP server success");
if (table === undefined) { if (table === undefined) {
table = []; table = [];
} }
@ -77,7 +77,7 @@ class LdapTable extends React.Component {
LdapBackend.deleteLdap(table[i]) LdapBackend.deleteLdap(table[i])
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", `Delete LDAP server success`); Setting.showMessage("success", "Delete LDAP server success");
table = Setting.deleteRow(table, i); table = Setting.deleteRow(table, i);
this.updateTable(table); this.updateTable(table);
} else { } else {
@ -103,7 +103,7 @@ class LdapTable extends React.Component {
<Link to={`/ldaps/${record.id}`}> <Link to={`/ldaps/${record.id}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
@ -113,7 +113,7 @@ class LdapTable extends React.Component {
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.host.localeCompare(b.host), sorter: (a, b) => a.host.localeCompare(b.host),
render: (text, record, index) => { render: (text, record, index) => {
return `${text}:${record.port}` return `${text}:${record.port}`;
} }
}, },
{ {
@ -131,7 +131,7 @@ class LdapTable extends React.Component {
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync), sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
render: (text, record, index) => { render: (text, record, index) => {
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : ( return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
<span style={{color: "#52c41a"}}>{text + " mins"}</span>) <span style={{color: "#52c41a"}}>{text + " mins"}</span>);
} }
}, },
{ {
@ -141,7 +141,7 @@ class LdapTable extends React.Component {
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync), sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
render: (text, record, index) => { render: (text, record, index) => {
return text return text;
} }
}, },
{ {
@ -165,13 +165,13 @@ class LdapTable extends React.Component {
type="danger">{i18next.t("general:Delete")}</Button> type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
return ( return (
<Table scroll={{x: 'max-content'}} rowKey="id" columns={columns} dataSource={table} size="middle" bordered pagination={false} <Table scroll={{x: "max-content"}} rowKey="id" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => ( title={() => (
<div> <div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp; {this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
@ -186,7 +186,7 @@ class LdapTable extends React.Component {
render() { render() {
return ( return (
<div> <div>
<Row style={{marginTop: '20px'}}> <Row style={{marginTop: "20px"}}>
<Col span={24}> <Col span={24}>
{ {
this.renderTable(this.props.table) this.renderTable(this.props.table)
@ -194,7 +194,7 @@ class LdapTable extends React.Component {
</Col> </Col>
</Row> </Row>
</div> </div>
) );
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd'; import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import * as ModelBackend from "./backend/ModelBackend"; import * as ModelBackend from "./backend/ModelBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -94,64 +94,64 @@ class ModelEditPage extends React.Component {
<div> <div>
{this.state.mode === "add" ? i18next.t("model:New Model") : i18next.t("model:Edit Model")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("model:New Model") : i18next.t("model:Edit Model")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteModel()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteModel()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> } style={(Setting.isMobile())? {margin: "5px"}:{}} type="inner">
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: "10px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.model.owner} onChange={(value => {this.updateModelField('owner', value);})}> <Select virtual={false} style={{width: "100%"}} value={this.state.model.owner} onChange={(value => {this.updateModelField("owner", value);})}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.model.name} onChange={e => { <Input value={this.state.model.name} onChange={e => {
this.updateModelField('name', e.target.value); this.updateModelField("name", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.model.displayName} onChange={e => { <Input value={this.state.model.displayName} onChange={e => {
this.updateModelField('displayName', e.target.value); this.updateModelField("displayName", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("model:Model text"), i18next.t("model:Model text - Tooltip"))} : {Setting.getLabel(i18next.t("model:Model text"), i18next.t("model:Model text - Tooltip"))} :
</Col> </Col>
<Col span={22}> <Col span={22}>
<TextArea rows={10} value={this.state.model.modelText} onChange={e => { <TextArea rows={10} value={this.state.model.modelText} onChange={e => {
this.updateModelField('modelText', e.target.value); this.updateModelField("modelText", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.model.isEnabled} onChange={checked => { <Switch checked={this.state.model.isEnabled} onChange={checked => {
this.updateModelField('isEnabled', checked); this.updateModelField("isEnabled", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
</Card> </Card>
) );
} }
submitModelEdit(willExist) { submitModelEdit(willExist) {
@ -159,19 +159,19 @@ class ModelEditPage extends React.Component {
ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model) ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model)
.then((res) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", "Successfully saved");
this.setState({ this.setState({
modelName: this.state.model.name, modelName: this.state.model.name,
}); });
if (willExist) { if (willExist) {
this.props.history.push(`/models`); this.props.history.push("/models");
} else { } else {
this.props.history.push(`/models/${this.state.model.owner}/${this.state.model.name}`); this.props.history.push(`/models/${this.state.model.owner}/${this.state.model.name}`);
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
this.updateModelField('name', this.state.modelName); this.updateModelField("name", this.state.modelName);
} }
}) })
.catch(error => { .catch(error => {
@ -182,7 +182,7 @@ class ModelEditPage extends React.Component {
deleteModel() { deleteModel() {
ModelBackend.deleteModel(this.state.model) ModelBackend.deleteModel(this.state.model)
.then(() => { .then(() => {
this.props.history.push(`/models`); this.props.history.push("/models");
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Model failed to delete: ${error}`); Setting.showMessage("error", `Model failed to delete: ${error}`);
@ -195,10 +195,10 @@ class ModelEditPage extends React.Component {
{ {
this.state.model !== null ? this.renderModel() : null this.state.model !== null ? this.renderModel() : null
} }
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteModel()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteModel()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd'; import {Button, Popconfirm, Switch, Table} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as ModelBackend from "./backend/ModelBackend"; import * as ModelBackend from "./backend/ModelBackend";
@ -31,7 +31,7 @@ class ModelListPage extends BaseListPage {
displayName: `New Model - ${randomName}`, displayName: `New Model - ${randomName}`,
modelText: "", modelText: "",
isEnabled: true, isEnabled: true,
} };
} }
addModel() { addModel() {
@ -49,7 +49,7 @@ class ModelListPage extends BaseListPage {
deleteModel(i) { deleteModel(i) {
ModelBackend.deleteModel(this.state.data[i]) ModelBackend.deleteModel(this.state.data[i])
.then((res) => { .then((res) => {
Setting.showMessage("success", `Model deleted successfully`); Setting.showMessage("success", "Model deleted successfully");
this.setState({ this.setState({
data: Setting.deleteRow(this.state.data, i), data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1}, pagination: {total: this.state.pagination.total - 1},
@ -65,40 +65,40 @@ class ModelListPage extends BaseListPage {
const columns = [ const columns = [
{ {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
dataIndex: 'owner', dataIndex: "owner",
key: 'owner', key: "owner",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('owner'), ...this.getColumnSearchProps("owner"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/organizations/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
width: '150px', width: "150px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/models/${text}`}> <Link to={`/models/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: "createdTime",
key: 'createdTime', key: "createdTime",
width: '160px', width: "160px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -106,43 +106,43 @@ class ModelListPage extends BaseListPage {
}, },
{ {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: 'displayName', dataIndex: "displayName",
key: 'displayName', key: "displayName",
width: '200px', width: "200px",
sorter: true, sorter: true,
...this.getColumnSearchProps('displayName'), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: i18next.t("general:Is enabled"), title: i18next.t("general:Is enabled"),
dataIndex: 'isEnabled', dataIndex: "isEnabled",
key: 'isEnabled', key: "isEnabled",
width: '120px', width: "120px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} /> <Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
) );
} }
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: "",
key: 'op', key: "op",
width: '170px', width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary"
onClick={() => this.props.history.push(`/models/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> onClick={() => this.props.history.push(`/models/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm <Popconfirm
title={`Sure to delete model: ${record.name} ?`} title={`Sure to delete model: ${record.name} ?`}
onConfirm={() => this.deleteModel(index)} onConfirm={() => this.deleteModel(index)}
> >
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button> <Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -156,7 +156,7 @@ class ModelListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={models} rowKey="name" size="middle" bordered <Table scroll={{x: "max-content"}} columns={columns} dataSource={models} rowKey="name" size="middle" bordered
pagination={paginationProps} pagination={paginationProps}
title={() => ( title={() => (
<div> <div>

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd'; import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as LdapBackend from "./backend/LdapBackend"; import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -53,7 +53,7 @@ class OrganizationEditPage extends React.Component {
getLdaps() { getLdaps() {
LdapBackend.getLdaps(this.state.organizationName) LdapBackend.getLdaps(this.state.organizationName)
.then(res => { .then(res => {
let resdata = [] let resdata = [];
if (res.status === "ok") { if (res.status === "ok") {
if (res.data !== null) { if (res.data !== null) {
resdata = res.data; resdata = res.data;
@ -61,8 +61,8 @@ class OrganizationEditPage extends React.Component {
} }
this.setState({ this.setState({
ldaps: resdata ldaps: resdata
}) });
}) });
} }
parseOrganizationField(key, value) { parseOrganizationField(key, value) {
@ -88,183 +88,183 @@ class OrganizationEditPage extends React.Component {
<div> <div>
{this.state.mode === "add" ? i18next.t("organization:New Organization") : i18next.t("organization:Edit Organization")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("organization:New Organization") : i18next.t("organization:Edit Organization")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> } style={(Setting.isMobile())? {margin: "5px"}:{}} type="inner">
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: "10px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.organization.name} disabled={this.state.organization.name === "built-in"} onChange={e => { <Input value={this.state.organization.name} disabled={this.state.organization.name === "built-in"} onChange={e => {
this.updateOrganizationField('name', e.target.value); this.updateOrganizationField("name", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.organization.displayName} onChange={e => { <Input value={this.state.organization.displayName} onChange={e => {
this.updateOrganizationField('displayName', e.target.value); this.updateOrganizationField("displayName", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} : {Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
<Input prefix={<LinkOutlined />} value={this.state.organization.favicon} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.organization.favicon} onChange={e => {
this.updateOrganizationField('favicon', e.target.value); this.updateOrganizationField("favicon", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.organization.favicon}> <a target="_blank" rel="noreferrer" href={this.state.organization.favicon}>
<img src={this.state.organization.favicon} alt={this.state.organization.favicon} height={90} style={{marginBottom: '20px'}}/> <img src={this.state.organization.favicon} alt={this.state.organization.favicon} height={90} style={{marginBottom: "20px"}} />
</a> </a>
</Col> </Col>
</Row> </Row>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Website URL"), i18next.t("organization:Website URL - Tooltip"))} : {Setting.getLabel(i18next.t("organization:Website URL"), i18next.t("organization:Website URL - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.organization.websiteUrl} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.organization.websiteUrl} onChange={e => {
this.updateOrganizationField('websiteUrl', e.target.value); this.updateOrganizationField("websiteUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password type"), i18next.t("general:Password type - Tooltip"))} : {Setting.getLabel(i18next.t("general:Password type"), i18next.t("general:Password type - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField('passwordType', value);})}> <Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}>
{ {
['plain', 'salt', 'md5-salt', 'bcrypt', 'pbkdf2-salt', 'argon2id'] ["plain", "salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>) .map((item, index) => <Option key={index} value={item}>{item}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password salt"), i18next.t("general:Password salt - Tooltip"))} : {Setting.getLabel(i18next.t("general:Password salt"), i18next.t("general:Password salt - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.organization.passwordSalt} onChange={e => { <Input value={this.state.organization.passwordSalt} onChange={e => {
this.updateOrganizationField('passwordSalt', e.target.value); this.updateOrganizationField("passwordSalt", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone prefix"), i18next.t("general:Phone prefix - Tooltip"))} : {Setting.getLabel(i18next.t("general:Phone prefix"), i18next.t("general:Phone prefix - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input addonBefore={"+"} value={this.state.organization.phonePrefix} onChange={e => { <Input addonBefore={"+"} value={this.state.organization.phonePrefix} onChange={e => {
this.updateOrganizationField('phonePrefix', e.target.value); this.updateOrganizationField("phonePrefix", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Default avatar"), i18next.t("general:Default avatar - Tooltip"))} : {Setting.getLabel(i18next.t("general:Default avatar"), i18next.t("general:Default avatar - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
<Input prefix={<LinkOutlined />} value={this.state.organization.defaultAvatar} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.organization.defaultAvatar} onChange={e => {
this.updateOrganizationField('defaultAvatar', e.target.value); this.updateOrganizationField("defaultAvatar", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.organization.defaultAvatar}> <a target="_blank" rel="noreferrer" href={this.state.organization.defaultAvatar}>
<img src={this.state.organization.defaultAvatar} alt={this.state.organization.defaultAvatar} height={90} style={{marginBottom: '20px'}}/> <img src={this.state.organization.defaultAvatar} alt={this.state.organization.defaultAvatar} height={90} style={{marginBottom: "20px"}} />
</a> </a>
</Col> </Col>
</Row> </Row>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} : {Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.organization.tags} onChange={(value => {this.updateOrganizationField('tags', value);})}> <Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.organization.tags} onChange={(value => {this.updateOrganizationField("tags", value);})}>
{ {
this.state.organization.tags?.map((item, index) => <Option key={index} value={item}>{item}</Option>) this.state.organization.tags?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Master password"), i18next.t("general:Master password - Tooltip"))} : {Setting.getLabel(i18next.t("general:Master password"), i18next.t("general:Master password - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.organization.masterPassword} onChange={e => { <Input value={this.state.organization.masterPassword} onChange={e => {
this.updateOrganizationField('masterPassword', e.target.value); this.updateOrganizationField("masterPassword", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Soft deletion"), i18next.t("organization:Soft deletion - Tooltip"))} : {Setting.getLabel(i18next.t("organization:Soft deletion"), i18next.t("organization:Soft deletion - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.organization.enableSoftDeletion} onChange={checked => { <Switch checked={this.state.organization.enableSoftDeletion} onChange={checked => {
this.updateOrganizationField('enableSoftDeletion', checked); this.updateOrganizationField("enableSoftDeletion", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Is profile public"), i18next.t("organization:Is profile public - Tooltip"))} : {Setting.getLabel(i18next.t("organization:Is profile public"), i18next.t("organization:Is profile public - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.organization.isProfilePublic} onChange={checked => { <Switch checked={this.state.organization.isProfilePublic} onChange={checked => {
this.updateOrganizationField('isProfilePublic', checked); this.updateOrganizationField("isProfilePublic", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} : {Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<AccountTable <AccountTable
title={i18next.t("organization:Account items")} title={i18next.t("organization:Account items")}
table={this.state.organization.accountItems} table={this.state.organization.accountItems}
onUpdateTable={(value) => { this.updateOrganizationField('accountItems', value)}} onUpdateTable={(value) => {this.updateOrganizationField("accountItems", value);}}
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}}> <Row style={{marginTop: "20px"}}>
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} : {Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
</Col> </Col>
<Col span={22}> <Col span={22}>
@ -273,12 +273,13 @@ class OrganizationEditPage extends React.Component {
table={this.state.ldaps} table={this.state.ldaps}
organizationName={this.state.organizationName} organizationName={this.state.organizationName}
onUpdateTable={(value) => { onUpdateTable={(value) => {
this.setState({ldaps: value}) }} this.setState({ldaps: value});
}}
/> />
</Col> </Col>
</Row> </Row>
</Card> </Card>
) );
} }
submitOrganizationEdit(willExist) { submitOrganizationEdit(willExist) {
@ -286,19 +287,19 @@ class OrganizationEditPage extends React.Component {
OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization) OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization)
.then((res) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", "Successfully saved");
this.setState({ this.setState({
organizationName: this.state.organization.name, organizationName: this.state.organization.name,
}); });
if (willExist) { if (willExist) {
this.props.history.push(`/organizations`); this.props.history.push("/organizations");
} else { } else {
this.props.history.push(`/organizations/${this.state.organization.name}`); this.props.history.push(`/organizations/${this.state.organization.name}`);
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
this.updateOrganizationField('name', this.state.organizationName); this.updateOrganizationField("name", this.state.organizationName);
} }
}) })
.catch(error => { .catch(error => {
@ -309,7 +310,7 @@ class OrganizationEditPage extends React.Component {
deleteOrganization() { deleteOrganization() {
OrganizationBackend.deleteOrganization(this.state.organization) OrganizationBackend.deleteOrganization(this.state.organization)
.then(() => { .then(() => {
this.props.history.push(`/organizations`); this.props.history.push("/organizations");
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`); Setting.showMessage("error", `Failed to connect to server: ${error}`);
@ -322,10 +323,10 @@ class OrganizationEditPage extends React.Component {
{ {
this.state.organization !== null ? this.renderOrganization() : null this.state.organization !== null ? this.renderOrganization() : null
} }
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd'; import {Button, Popconfirm, Result, Switch, Table} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
@ -22,7 +22,6 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class OrganizationListPage extends BaseListPage { class OrganizationListPage extends BaseListPage {
newOrganization() { newOrganization() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
return { return {
@ -58,6 +57,8 @@ class OrganizationListPage extends BaseListPage {
{name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"}, {name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"}, {name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
@ -65,7 +66,7 @@ class OrganizationListPage extends BaseListPage {
{name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"},
], ],
} };
} }
addOrganization() { addOrganization() {
@ -83,7 +84,7 @@ class OrganizationListPage extends BaseListPage {
deleteOrganization(i) { deleteOrganization(i) {
OrganizationBackend.deleteOrganization(this.state.data[i]) OrganizationBackend.deleteOrganization(this.state.data[i])
.then((res) => { .then((res) => {
Setting.showMessage("success", `Organization deleted successfully`); Setting.showMessage("success", "Organization deleted successfully");
this.setState({ this.setState({
data: Setting.deleteRow(this.state.data, i), data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1}, pagination: {total: this.state.pagination.total - 1},
@ -99,25 +100,25 @@ class OrganizationListPage extends BaseListPage {
const columns = [ const columns = [
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
width: '120px', width: "120px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/organizations/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: "createdTime",
key: 'createdTime', key: "createdTime",
width: '160px', width: "160px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -125,106 +126,106 @@ class OrganizationListPage extends BaseListPage {
}, },
{ {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: 'displayName', dataIndex: "displayName",
key: 'displayName', key: "displayName",
// width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('displayName'), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: i18next.t("organization:Favicon"), title: i18next.t("organization:Favicon"),
dataIndex: 'favicon', dataIndex: "favicon",
key: 'favicon', key: "favicon",
width: '50px', width: "50px",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<a target="_blank" rel="noreferrer" href={text}> <a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={40} /> <img src={text} alt={text} width={40} />
</a> </a>
) );
} }
}, },
{ {
title: i18next.t("organization:Website URL"), title: i18next.t("organization:Website URL"),
dataIndex: 'websiteUrl', dataIndex: "websiteUrl",
key: 'websiteUrl', key: "websiteUrl",
width: '300px', width: "300px",
sorter: true, sorter: true,
...this.getColumnSearchProps('websiteUrl'), ...this.getColumnSearchProps("websiteUrl"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<a target="_blank" rel="noreferrer" href={text}> <a target="_blank" rel="noreferrer" href={text}>
{text} {text}
</a> </a>
) );
} }
}, },
{ {
title: i18next.t("general:Password type"), title: i18next.t("general:Password type"),
dataIndex: 'passwordType', dataIndex: "passwordType",
key: 'passwordType', key: "passwordType",
width: '150px', width: "150px",
sorter: true, sorter: true,
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: 'plain', value: 'plain'}, {text: "plain", value: "plain"},
{text: 'salt', value: 'salt'}, {text: "salt", value: "salt"},
{text: 'md5-salt', value: 'md5-salt'}, {text: "md5-salt", value: "md5-salt"},
], ],
}, },
{ {
title: i18next.t("general:Password salt"), title: i18next.t("general:Password salt"),
dataIndex: 'passwordSalt', dataIndex: "passwordSalt",
key: 'passwordSalt', key: "passwordSalt",
width: '150px', width: "150px",
sorter: true, sorter: true,
...this.getColumnSearchProps('passwordSalt'), ...this.getColumnSearchProps("passwordSalt"),
}, },
{ {
title: i18next.t("organization:Default avatar"), title: i18next.t("organization:Default avatar"),
dataIndex: 'defaultAvatar', dataIndex: "defaultAvatar",
key: 'defaultAvatar', key: "defaultAvatar",
width: '120px', width: "120px",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<a target="_blank" rel="noreferrer" href={text}> <a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={40} /> <img src={text} alt={text} width={40} />
</a> </a>
) );
} }
}, },
{ {
title: i18next.t("organization:Soft deletion"), title: i18next.t("organization:Soft deletion"),
dataIndex: 'enableSoftDeletion', dataIndex: "enableSoftDeletion",
key: 'enableSoftDeletion', key: "enableSoftDeletion",
width: '140px', width: "140px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} /> <Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
) );
} }
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: "",
key: 'op', key: "op",
width: '240px', width: "240px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/organizations/${record.name}/users`)}>{i18next.t("general:Users")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/organizations/${record.name}/users`)}>{i18next.t("general:Users")}</Button>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} onClick={() => this.props.history.push(`/organizations/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/organizations/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm <Popconfirm
title={`Sure to delete organization: ${record.name} ?`} title={`Sure to delete organization: ${record.name} ?`}
onConfirm={() => this.deleteOrganization(index)} onConfirm={() => this.deleteOrganization(index)}
disabled={record.name === "built-in"} disabled={record.name === "built-in"}
> >
<Button style={{marginBottom: '10px'}} disabled={record.name === "built-in"} type="danger">{i18next.t("general:Delete")}</Button> <Button style={{marginBottom: "10px"}} disabled={record.name === "built-in"} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -236,9 +237,20 @@ class OrganizationListPage extends BaseListPage {
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total), showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
}; };
if (!this.state.isAuthorized) {
return (
<Result
status="403"
title="403 Unauthorized"
subTitle={i18next.t("general:Sorry, you do not have permission to access this page.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
/>
);
}
return ( return (
<div> <div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={organizations} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={organizations} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Organizations")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Organizations")}&nbsp;&nbsp;&nbsp;&nbsp;
@ -273,6 +285,13 @@ class OrganizationListPage extends BaseListPage {
searchText: params.searchText, searchText: params.searchText,
searchedColumn: params.searchedColumn, searchedColumn: params.searchedColumn,
}); });
} else {
if (res.msg.includes("Unauthorized")) {
this.setState({
loading: false,
isAuthorized: false,
});
}
} }
}); });
}; };

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {Button, Col, Modal, Row, Input,} from "antd"; import {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import React from "react"; import React from "react";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
@ -50,10 +50,9 @@ export const PasswordModal = (props) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Password Set")); Setting.showMessage("success", i18next.t("user:Password Set"));
setVisible(false); setVisible(false);
} } else {Setting.showMessage("error", i18next.t(`user:${res.msg}`));}
else Setting.showMessage("error", i18next.t(`user:${res.msg}`)); });
}) };
}
let hasOldPassword = user.password !== ""; let hasOldPassword = user.password !== "";
@ -88,7 +87,7 @@ export const PasswordModal = (props) => {
</Col> </Col>
</Modal> </Modal>
</Row> </Row>
) );
} };
export default PasswordModal; export default PasswordModal;

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Descriptions, Input, Modal, Row, Select} from 'antd'; import {Button, Card, Col, Descriptions, Input, Modal, Row, Select} from "antd";
import {InfoCircleTwoTone} from "@ant-design/icons"; import {InfoCircleTwoTone} from "@ant-design/icons";
import * as PaymentBackend from "./backend/PaymentBackend"; import * as PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -79,7 +79,7 @@ class PaymentEditPage extends React.Component {
isInvoiceLoading: false, isInvoiceLoading: false,
}); });
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully invoiced`); Setting.showMessage("success", "Successfully invoiced");
Setting.openLinkSafe(res.data); Setting.openLinkSafe(res.data);
this.getPayment(); this.getPayment();
} else { } else {
@ -140,7 +140,7 @@ class PaymentEditPage extends React.Component {
</Descriptions> </Descriptions>
</p> </p>
</Modal> </Modal>
) );
} }
renderPayment() { renderPayment() {
@ -149,12 +149,12 @@ class PaymentEditPage extends React.Component {
<div> <div>
{this.state.mode === "add" ? i18next.t("payment:New Payment") : i18next.t("payment:Edit Payment")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("payment:New Payment") : i18next.t("payment:Edit Payment")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> } style={(Setting.isMobile())? {margin: "5px"}:{}} type="inner">
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: "10px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -163,8 +163,8 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -173,18 +173,18 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={true} value={this.state.payment.displayName} onChange={e => { <Input disabled={true} value={this.state.payment.displayName} onChange={e => {
this.updatePaymentField('displayName', e.target.value); this.updatePaymentField("displayName", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} : {Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -193,8 +193,8 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Type"), i18next.t("payment:Type - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Type"), i18next.t("payment:Type - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -203,8 +203,8 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Product"), i18next.t("payment:Product - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Product"), i18next.t("payment:Product - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -213,8 +213,8 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Price"), i18next.t("payment:Price - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Price"), i18next.t("payment:Price - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -223,8 +223,8 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -233,8 +233,8 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:State"), i18next.t("payment:State - Tooltip"))} : {Setting.getLabel(i18next.t("payment:State"), i18next.t("payment:State - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -243,8 +243,8 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Message"), i18next.t("payment:Message - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Message"), i18next.t("payment:Message - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -253,113 +253,113 @@ class PaymentEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Person name"), i18next.t("payment:Person name - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Person name"), i18next.t("payment:Person name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personName} onChange={e => { <Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personName} onChange={e => {
this.updatePaymentField('personName', e.target.value); this.updatePaymentField("personName", e.target.value);
if (this.state.payment.invoiceType === "Individual") { if (this.state.payment.invoiceType === "Individual") {
this.updatePaymentField('invoiceTitle', e.target.value); this.updatePaymentField("invoiceTitle", e.target.value);
this.updatePaymentField('invoiceTaxId', ""); this.updatePaymentField("invoiceTaxId", "");
} }
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Person ID card"), i18next.t("payment:Person ID card - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Person ID card"), i18next.t("payment:Person ID card - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personIdCard} onChange={e => { <Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personIdCard} onChange={e => {
this.updatePaymentField('personIdCard', e.target.value); this.updatePaymentField("personIdCard", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Person Email"), i18next.t("payment:Person Email - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Person Email"), i18next.t("payment:Person Email - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personEmail} onChange={e => { <Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personEmail} onChange={e => {
this.updatePaymentField('personEmail', e.target.value); this.updatePaymentField("personEmail", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Person phone"), i18next.t("payment:Person phone - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Person phone"), i18next.t("payment:Person phone - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personPhone} onChange={e => { <Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personPhone} onChange={e => {
this.updatePaymentField('personPhone', e.target.value); this.updatePaymentField("personPhone", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Invoice type"), i18next.t("payment:Invoice type - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Invoice type"), i18next.t("payment:Invoice type - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select disabled={this.state.payment.invoiceUrl !== ""} virtual={false} style={{width: '100%'}} value={this.state.payment.invoiceType} onChange={(value => { <Select disabled={this.state.payment.invoiceUrl !== ""} virtual={false} style={{width: "100%"}} value={this.state.payment.invoiceType} onChange={(value => {
this.updatePaymentField('invoiceType', value); this.updatePaymentField("invoiceType", value);
if (value === "Individual") { if (value === "Individual") {
this.updatePaymentField('invoiceTitle', this.state.payment.personName); this.updatePaymentField("invoiceTitle", this.state.payment.personName);
this.updatePaymentField('invoiceTaxId', ""); this.updatePaymentField("invoiceTaxId", "");
} }
})}> })}>
{ {
[ [
{id: 'Individual', name: i18next.t("payment:Individual")}, {id: "Individual", name: i18next.t("payment:Individual")},
{id: 'Organization', name: i18next.t("payment:Organization")}, {id: "Organization", name: i18next.t("payment:Organization")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Invoice title"), i18next.t("payment:Invoice title - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Invoice title"), i18next.t("payment:Invoice title - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={this.state.payment.invoiceUrl !== "" || this.state.payment.invoiceType === "Individual"} value={this.state.payment.invoiceTitle} onChange={e => { <Input disabled={this.state.payment.invoiceUrl !== "" || this.state.payment.invoiceType === "Individual"} value={this.state.payment.invoiceTitle} onChange={e => {
this.updatePaymentField('invoiceTitle', e.target.value); this.updatePaymentField("invoiceTitle", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Invoice tax ID"), i18next.t("payment:Invoice tax ID - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Invoice tax ID"), i18next.t("payment:Invoice tax ID - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={this.state.payment.invoiceUrl !== "" || this.state.payment.invoiceType === "Individual"} value={this.state.payment.invoiceTaxId} onChange={e => { <Input disabled={this.state.payment.invoiceUrl !== "" || this.state.payment.invoiceType === "Individual"} value={this.state.payment.invoiceTaxId} onChange={e => {
this.updatePaymentField('invoiceTaxId', e.target.value); this.updatePaymentField("invoiceTaxId", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Invoice remark"), i18next.t("payment:Invoice remark - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Invoice remark"), i18next.t("payment:Invoice remark - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.invoiceRemark} onChange={e => { <Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.invoiceRemark} onChange={e => {
this.updatePaymentField('invoiceRemark', e.target.value); this.updatePaymentField("invoiceRemark", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Invoice URL"), i18next.t("payment:Invoice URL - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Invoice URL"), i18next.t("payment:Invoice URL - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input disabled={true} value={this.state.payment.invoiceUrl} onChange={e => { <Input disabled={true} value={this.state.payment.invoiceUrl} onChange={e => {
this.updatePaymentField('invoiceUrl', e.target.value); this.updatePaymentField("invoiceUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row id={"invoice-area"} style={{marginTop: '20px'}} > <Row id={"invoice-area"} style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Invoice actions"), i18next.t("payment:Invoice actions - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Invoice actions"), i18next.t("payment:Invoice actions - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
@ -384,7 +384,7 @@ class PaymentEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
</Card> </Card>
) );
} }
checkError() { checkError() {
@ -444,19 +444,19 @@ class PaymentEditPage extends React.Component {
PaymentBackend.updatePayment(this.state.payment.owner, this.state.paymentName, payment) PaymentBackend.updatePayment(this.state.payment.owner, this.state.paymentName, payment)
.then((res) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", "Successfully saved");
this.setState({ this.setState({
paymentName: this.state.payment.name, paymentName: this.state.payment.name,
}); });
if (willExist) { if (willExist) {
this.props.history.push(`/payments`); this.props.history.push("/payments");
} else { } else {
this.props.history.push(`/payments/${this.state.payment.name}`); this.props.history.push(`/payments/${this.state.payment.name}`);
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
this.updatePaymentField('name', this.state.paymentName); this.updatePaymentField("name", this.state.paymentName);
} }
}) })
.catch(error => { .catch(error => {
@ -467,7 +467,7 @@ class PaymentEditPage extends React.Component {
deletePayment() { deletePayment() {
PaymentBackend.deletePayment(this.state.payment) PaymentBackend.deletePayment(this.state.payment)
.then(() => { .then(() => {
this.props.history.push(`/payments`); this.props.history.push("/payments");
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Payment failed to delete: ${error}`); Setting.showMessage("error", `Payment failed to delete: ${error}`);
@ -483,10 +483,10 @@ class PaymentEditPage extends React.Component {
{ {
this.renderModal() this.renderModal()
} }
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Popconfirm, Table} from 'antd'; import {Button, Popconfirm, Table} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as PaymentBackend from "./backend/PaymentBackend"; import * as PaymentBackend from "./backend/PaymentBackend";
@ -44,7 +44,7 @@ class PaymentListPage extends BaseListPage {
returnUrl: "https://door.casdoor.com/payments", returnUrl: "https://door.casdoor.com/payments",
state: "Paid", state: "Paid",
message: "", message: "",
} };
} }
addPayment() { addPayment() {
@ -62,7 +62,7 @@ class PaymentListPage extends BaseListPage {
deletePayment(i) { deletePayment(i) {
PaymentBackend.deletePayment(this.state.data[i]) PaymentBackend.deletePayment(this.state.data[i])
.then((res) => { .then((res) => {
Setting.showMessage("success", `Payment deleted successfully`); Setting.showMessage("success", "Payment deleted successfully");
this.setState({ this.setState({
data: Setting.deleteRow(this.state.data, i), data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1}, pagination: {total: this.state.pagination.total - 1},
@ -78,55 +78,55 @@ class PaymentListPage extends BaseListPage {
const columns = [ const columns = [
{ {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
dataIndex: 'organization', dataIndex: "organization",
key: 'organization', key: "organization",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('organization'), ...this.getColumnSearchProps("organization"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/organizations/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:User"), title: i18next.t("general:User"),
dataIndex: 'user', dataIndex: "user",
key: 'user', key: "user",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('user'), ...this.getColumnSearchProps("user"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/users/${record.organization}/${text}`}> <Link to={`/users/${record.organization}/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
width: '180px', width: "180px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/payments/${text}`}> <Link to={`/payments/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: "createdTime",
key: 'createdTime', key: "createdTime",
width: '160px', width: "160px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -142,28 +142,28 @@ class PaymentListPage extends BaseListPage {
// }, // },
{ {
title: i18next.t("general:Provider"), title: i18next.t("general:Provider"),
dataIndex: 'provider', dataIndex: "provider",
key: 'provider', key: "provider",
width: '150px', width: "150px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('provider'), ...this.getColumnSearchProps("provider"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/providers/${text}`}> <Link to={`/providers/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("payment:Type"), title: i18next.t("payment:Type"),
dataIndex: 'type', dataIndex: "type",
key: 'type', key: "type",
width: '140px', width: "140px",
align: 'center', align: "center",
filterMultiple: false, filterMultiple: false,
filters: Setting.getProviderTypeOptions('Payment').map((o) => {return {text:o.id, value:o.name}}), filters: Setting.getProviderTypeOptions("Payment").map((o) => {return {text:o.id, value:o.name};}),
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
record.category = "Payment"; record.category = "Payment";
@ -172,55 +172,55 @@ class PaymentListPage extends BaseListPage {
}, },
{ {
title: i18next.t("payment:Product"), title: i18next.t("payment:Product"),
dataIndex: 'productDisplayName', dataIndex: "productDisplayName",
key: 'productDisplayName', key: "productDisplayName",
// width: '160px', // width: '160px',
sorter: true, sorter: true,
...this.getColumnSearchProps('productDisplayName'), ...this.getColumnSearchProps("productDisplayName"),
}, },
{ {
title: i18next.t("payment:Price"), title: i18next.t("payment:Price"),
dataIndex: 'price', dataIndex: "price",
key: 'price', key: "price",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('price'), ...this.getColumnSearchProps("price"),
}, },
{ {
title: i18next.t("payment:Currency"), title: i18next.t("payment:Currency"),
dataIndex: 'currency', dataIndex: "currency",
key: 'currency', key: "currency",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('currency'), ...this.getColumnSearchProps("currency"),
}, },
{ {
title: i18next.t("payment:State"), title: i18next.t("payment:State"),
dataIndex: 'state', dataIndex: "state",
key: 'state', key: "state",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('state'), ...this.getColumnSearchProps("state"),
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: "",
key: 'op', key: "op",
width: '240px', width: "240px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} onClick={() => this.props.history.push(`/payments/${record.name}/result`)}>{i18next.t("payment:Result")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/payments/${record.name}/result`)}>{i18next.t("payment:Result")}</Button>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/payments/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/payments/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm <Popconfirm
title={`Sure to delete payment: ${record.name} ?`} title={`Sure to delete payment: ${record.name} ?`}
onConfirm={() => this.deletePayment(index)} onConfirm={() => this.deletePayment(index)}
> >
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button> <Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -234,7 +234,7 @@ class PaymentListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={payments} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={payments} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Payments")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Payments")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Result, Spin} from 'antd'; import {Button, Result, Spin} from "antd";
import * as PaymentBackend from "./backend/PaymentBackend"; import * as PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
@ -71,7 +71,7 @@ class PaymentResultPage extends React.Component {
]} ]}
/> />
</div> </div>
) );
} else if (payment.state === "Created") { } else if (payment.state === "Created") {
return ( return (
<div> <div>
@ -87,7 +87,7 @@ class PaymentResultPage extends React.Component {
]} ]}
/> />
</div> </div>
) );
} else { } else {
return ( return (
<div> <div>
@ -107,7 +107,7 @@ class PaymentResultPage extends React.Component {
]} ]}
/> />
</div> </div>
) );
} }
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd'; import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import * as PermissionBackend from "./backend/PermissionBackend"; import * as PermissionBackend from "./backend/PermissionBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
@ -21,6 +21,7 @@ import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend"; import * as RoleBackend from "./backend/RoleBackend";
import * as ModelBackend from "./backend/ModelBackend"; import * as ModelBackend from "./backend/ModelBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
const {Option} = Select; const {Option} = Select;
@ -36,6 +37,7 @@ class PermissionEditPage extends React.Component {
users: [], users: [],
roles: [], roles: [],
models: [], models: [],
resources: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
} }
@ -55,6 +57,7 @@ class PermissionEditPage extends React.Component {
this.getUsers(permission.owner); this.getUsers(permission.owner);
this.getRoles(permission.owner); this.getRoles(permission.owner);
this.getModels(permission.owner); this.getModels(permission.owner);
this.getResources(permission.owner);
}); });
} }
@ -94,6 +97,15 @@ class PermissionEditPage extends React.Component {
}); });
} }
getResources(organizationName) {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
resources: (res.msg === undefined) ? res : [],
});
});
}
parsePermissionField(key, value) { parsePermissionField(key, value) {
if ([""].includes(key)) { if ([""].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
@ -117,20 +129,22 @@ class PermissionEditPage extends React.Component {
<div> <div>
{this.state.mode === "add" ? i18next.t("permission:New Permission") : i18next.t("permission:Edit Permission")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("permission:New Permission") : i18next.t("permission:Edit Permission")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> } style={(Setting.isMobile())? {margin: "5px"}:{}} type="inner">
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: "10px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.owner} onChange={(owner => { <Select virtual={false} style={{width: "100%"}} value={this.state.permission.owner} onChange={(owner => {
this.updatePermissionField('owner', owner); this.updatePermissionField("owner", owner);
this.getUsers(owner); this.getUsers(owner);
this.getRoles(owner); this.getRoles(owner);
this.getModels(owner);
this.getResources(owner);
})}> })}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
@ -138,33 +152,33 @@ class PermissionEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.permission.name} onChange={e => { <Input value={this.state.permission.name} onChange={e => {
this.updatePermissionField('name', e.target.value); this.updatePermissionField("name", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.permission.displayName} onChange={e => { <Input value={this.state.permission.displayName} onChange={e => {
this.updatePermissionField('displayName', e.target.value); this.updatePermissionField("displayName", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Model"), i18next.t("general:Model - Tooltip"))} : {Setting.getLabel(i18next.t("general:Model"), i18next.t("general:Model - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.model} onChange={(model => { <Select virtual={false} style={{width: "100%"}} value={this.state.permission.model} onChange={(model => {
this.updatePermissionField('model', model); this.updatePermissionField("model", model);
})}> })}>
{ {
this.state.models.map((model, index) => <Option key={index} value={model.name}>{model.name}</Option>) this.state.models.map((model, index) => <Option key={index} value={model.name}>{model.name}</Option>)
@ -172,93 +186,105 @@ class PermissionEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} : {Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.permission.users} onChange={(value => {this.updatePermissionField('users', value);})}> <Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.permission.users} onChange={(value => {this.updatePermissionField("users", value);})}>
{ {
this.state.users.map((user, index) => <Option key={index} value={`${user.owner}/${user.name}`}>{`${user.owner}/${user.name}`}</Option>) this.state.users.map((user, index) => <Option key={index} value={`${user.owner}/${user.name}`}>{`${user.owner}/${user.name}`}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} : {Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.permission.roles} onChange={(value => {this.updatePermissionField('roles', value);})}> <Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.permission.roles} onChange={(value => {this.updatePermissionField("roles", value);})}>
{ {
this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission, index) => <Option key={index} value={`${permission.owner}/${permission.name}`}>{`${permission.owner}/${permission.name}`}</Option>) this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission, index) => <Option key={index} value={`${permission.owner}/${permission.name}`}>{`${permission.owner}/${permission.name}`}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Resource type"), i18next.t("permission:Resource type - Tooltip"))} : {Setting.getLabel(i18next.t("permission:Resource type"), i18next.t("permission:Resource type - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.resourceType} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.permission.resourceType} onChange={(value => {
this.updatePermissionField('resourceType', value); this.updatePermissionField("resourceType", value);
})}> })}>
{ {
[ [
{id: 'Application', name: 'Application'}, {id: "Application", name: "Application"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Resources"), i18next.t("permission:Resources - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.permission.resources} onChange={(value => {this.updatePermissionField("resources", value);})}>
{
this.state.resources.map((resource, index) => <Option key={index} value={`${resource.name}`}>{`${resource.name}`}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Actions"), i18next.t("permission:Actions - Tooltip"))} : {Setting.getLabel(i18next.t("permission:Actions"), i18next.t("permission:Actions - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.permission.actions} onChange={(value => { <Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.permission.actions} onChange={(value => {
this.updatePermissionField('actions', value); this.updatePermissionField("actions", value);
})}> })}>
{ {
[ [
{id: 'Read', name: 'Read'}, {id: "Read", name: "Read"},
{id: 'Write', name: 'Write'}, {id: "Write", name: "Write"},
{id: 'Admin', name: 'Admin'}, {id: "Admin", name: "Admin"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Effect"), i18next.t("permission:Effect - Tooltip"))} : {Setting.getLabel(i18next.t("permission:Effect"), i18next.t("permission:Effect - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.effect} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.permission.effect} onChange={(value => {
this.updatePermissionField('effect', value); this.updatePermissionField("effect", value);
})}> })}>
{ {
[ [
{id: 'Allow', name: 'Allow'}, {id: "Allow", name: "Allow"},
{id: 'Deny', name: 'Deny'}, {id: "Deny", name: "Deny"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.permission.isEnabled} onChange={checked => { <Switch checked={this.state.permission.isEnabled} onChange={checked => {
this.updatePermissionField('isEnabled', checked); this.updatePermissionField("isEnabled", checked);
}} /> }} />
</Col> </Col>
</Row> </Row>
</Card> </Card>
) );
} }
submitPermissionEdit(willExist) { submitPermissionEdit(willExist) {
@ -266,19 +292,19 @@ class PermissionEditPage extends React.Component {
PermissionBackend.updatePermission(this.state.organizationName, this.state.permissionName, permission) PermissionBackend.updatePermission(this.state.organizationName, this.state.permissionName, permission)
.then((res) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", "Successfully saved");
this.setState({ this.setState({
permissionName: this.state.permission.name, permissionName: this.state.permission.name,
}); });
if (willExist) { if (willExist) {
this.props.history.push(`/permissions`); this.props.history.push("/permissions");
} else { } else {
this.props.history.push(`/permissions/${this.state.permission.owner}/${this.state.permission.name}`); this.props.history.push(`/permissions/${this.state.permission.owner}/${this.state.permission.name}`);
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
this.updatePermissionField('name', this.state.permissionName); this.updatePermissionField("name", this.state.permissionName);
} }
}) })
.catch(error => { .catch(error => {
@ -289,7 +315,7 @@ class PermissionEditPage extends React.Component {
deletePermission() { deletePermission() {
PermissionBackend.deletePermission(this.state.permission) PermissionBackend.deletePermission(this.state.permission)
.then(() => { .then(() => {
this.props.history.push(`/permissions`); this.props.history.push("/permissions");
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Permission failed to delete: ${error}`); Setting.showMessage("error", `Permission failed to delete: ${error}`);
@ -302,10 +328,10 @@ class PermissionEditPage extends React.Component {
{ {
this.state.permission !== null ? this.renderPermission() : null this.state.permission !== null ? this.renderPermission() : null
} }
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd'; import {Button, Popconfirm, Switch, Table} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as PermissionBackend from "./backend/PermissionBackend"; import * as PermissionBackend from "./backend/PermissionBackend";
@ -33,10 +33,10 @@ class PermissionListPage extends BaseListPage {
roles: [], roles: [],
resourceType: "Application", resourceType: "Application",
resources: ["app-built-in"], resources: ["app-built-in"],
action: "Read", actions: ["Read"],
effect: "Allow", effect: "Allow",
isEnabled: true, isEnabled: true,
} };
} }
addPermission() { addPermission() {
@ -54,7 +54,7 @@ class PermissionListPage extends BaseListPage {
deletePermission(i) { deletePermission(i) {
PermissionBackend.deletePermission(this.state.data[i]) PermissionBackend.deletePermission(this.state.data[i])
.then((res) => { .then((res) => {
Setting.showMessage("success", `Permission deleted successfully`); Setting.showMessage("success", "Permission deleted successfully");
this.setState({ this.setState({
data: Setting.deleteRow(this.state.data, i), data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1}, pagination: {total: this.state.pagination.total - 1},
@ -70,40 +70,40 @@ class PermissionListPage extends BaseListPage {
const columns = [ const columns = [
{ {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
dataIndex: 'owner', dataIndex: "owner",
key: 'owner', key: "owner",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('owner'), ...this.getColumnSearchProps("owner"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/organizations/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
width: '150px', width: "150px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/permissions/${text}`}> <Link to={`/permissions/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: "createdTime",
key: 'createdTime', key: "createdTime",
width: '160px', width: "160px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -111,109 +111,109 @@ class PermissionListPage extends BaseListPage {
}, },
{ {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: 'displayName', dataIndex: "displayName",
key: 'displayName', key: "displayName",
width: '160px', width: "160px",
sorter: true, sorter: true,
...this.getColumnSearchProps('displayName'), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: i18next.t("role:Sub users"), title: i18next.t("role:Sub users"),
dataIndex: 'users', dataIndex: "users",
key: 'users', key: "users",
// width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('users'), ...this.getColumnSearchProps("users"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text);
} }
}, },
{ {
title: i18next.t("role:Sub roles"), title: i18next.t("role:Sub roles"),
dataIndex: 'roles', dataIndex: "roles",
key: 'roles', key: "roles",
// width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('roles'), ...this.getColumnSearchProps("roles"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text);
} }
}, },
{ {
title: i18next.t("permission:Resource type"), title: i18next.t("permission:Resource type"),
dataIndex: 'resourceType', dataIndex: "resourceType",
key: 'resourceType', key: "resourceType",
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: 'Application', value: 'Application'}, {text: "Application", value: "Application"},
], ],
width: '170px', width: "170px",
sorter: true, sorter: true,
}, },
{ {
title: i18next.t("permission:Resources"), title: i18next.t("permission:Resources"),
dataIndex: 'resources', dataIndex: "resources",
key: 'resources', key: "resources",
// width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('resources'), ...this.getColumnSearchProps("resources"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text);
} }
}, },
{ {
title: i18next.t("permission:Actions"), title: i18next.t("permission:Actions"),
dataIndex: 'actions', dataIndex: "actions",
key: 'actions', key: "actions",
// width: '100px', // width: '100px',
sorter: true, sorter: true,
...this.getColumnSearchProps('actions'), ...this.getColumnSearchProps("actions"),
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getTags(text); return Setting.getTags(text);
} }
}, },
{ {
title: i18next.t("permission:Effect"), title: i18next.t("permission:Effect"),
dataIndex: 'effect', dataIndex: "effect",
key: 'effect', key: "effect",
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: 'Allow', value: 'Allow'}, {text: "Allow", value: "Allow"},
{text: 'Deny', value: 'Deny'}, {text: "Deny", value: "Deny"},
], ],
width: '120px', width: "120px",
sorter: true, sorter: true,
}, },
{ {
title: i18next.t("general:Is enabled"), title: i18next.t("general:Is enabled"),
dataIndex: 'isEnabled', dataIndex: "isEnabled",
key: 'isEnabled', key: "isEnabled",
width: '120px', width: "120px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} /> <Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
) );
} }
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: "",
key: 'op', key: "op",
width: '170px', width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/permissions/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/permissions/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm <Popconfirm
title={`Sure to delete permission: ${record.name} ?`} title={`Sure to delete permission: ${record.name} ?`}
onConfirm={() => this.deletePermission(index)} onConfirm={() => this.deletePermission(index)}
> >
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button> <Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -227,7 +227,7 @@ class PermissionListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={permissions} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={permissions} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -94,7 +94,7 @@ class ProductBuyPage extends React.Component {
let providerMap = {}; let providerMap = {};
this.state.providers.forEach(provider => { this.state.providers.forEach(provider => {
providerMap[provider.name] = provider; providerMap[provider.name] = provider;
}) });
return product.providers.map(providerName => providerMap[providerName]); return product.providers.map(providerName => providerMap[providerName]);
} }
@ -153,7 +153,7 @@ class ProductBuyPage extends React.Component {
text text
} }
</Button> </Button>
) );
} }
renderProviderButton(provider, product) { renderProviderButton(provider, product) {
@ -165,7 +165,7 @@ class ProductBuyPage extends React.Component {
} }
</span> </span>
</span> </span>
) );
} }
renderPay(product) { renderPay(product) {
@ -183,7 +183,7 @@ class ProductBuyPage extends React.Component {
const providers = this.getProviders(product); const providers = this.getProviders(product);
return providers.map(provider => { return providers.map(provider => {
return this.renderProviderButton(provider, product); return this.renderProviderButton(provider, product);
}) });
} }
render() { render() {
@ -206,7 +206,7 @@ class ProductBuyPage extends React.Component {
<Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item> <Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item> <Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Image")} span={3}> <Descriptions.Item label={i18next.t("product:Image")} span={3}>
<img src={product?.image} alt={product?.name} height={90} style={{marginBottom: '20px'}}/> <img src={product?.image} alt={product?.name} height={90} style={{marginBottom: "20px"}} />
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")}> <Descriptions.Item label={i18next.t("product:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}> <span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
@ -225,7 +225,7 @@ class ProductBuyPage extends React.Component {
</Descriptions> </Descriptions>
</Spin> </Spin>
</div> </div>
) );
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd'; import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
import * as ProductBackend from "./backend/ProductBackend"; import * as ProductBackend from "./backend/ProductBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
@ -82,165 +82,165 @@ class ProductEditPage extends React.Component {
<div> <div>
{this.state.mode === "add" ? i18next.t("product:New Product") : i18next.t("product:Edit Product")}&nbsp;&nbsp;&nbsp;&nbsp; {this.state.mode === "add" ? i18next.t("product:New Product") : i18next.t("product:Edit Product")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> } style={(Setting.isMobile())? {margin: "5px"}:{}} type="inner">
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.name} onChange={e => { <Input value={this.state.product.name} onChange={e => {
this.updateProductField('name', e.target.value); this.updateProductField("name", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.displayName} onChange={e => { <Input value={this.state.product.displayName} onChange={e => {
this.updateProductField('displayName', e.target.value); this.updateProductField("displayName", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} : {Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
</Col> </Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: '100%'} :{}}> <Col span={22} style={(Setting.isMobile()) ? {maxWidth: "100%"} :{}}>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
<Input prefix={<LinkOutlined />} value={this.state.product.image} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.product.image} onChange={e => {
this.updateProductField('image', e.target.value); this.updateProductField("image", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.product.image}> <a target="_blank" rel="noreferrer" href={this.state.product.image}>
<img src={this.state.product.image} alt={this.state.product.image} height={90} style={{marginBottom: '20px'}}/> <img src={this.state.product.image} alt={this.state.product.image} height={90} style={{marginBottom: "20px"}} />
</a> </a>
</Col> </Col>
</Row> </Row>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Tag"), i18next.t("product:Tag - Tooltip"))} : {Setting.getLabel(i18next.t("product:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.tag} onChange={e => { <Input value={this.state.product.tag} onChange={e => {
this.updateProductField('tag', e.target.value); this.updateProductField("tag", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Detail"), i18next.t("product:Detail - Tooltip"))} : {Setting.getLabel(i18next.t("product:Detail"), i18next.t("product:Detail - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.detail} onChange={e => { <Input value={this.state.product.detail} onChange={e => {
this.updateProductField('detail', e.target.value); this.updateProductField("detail", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Currency"), i18next.t("product:Currency - Tooltip"))} : {Setting.getLabel(i18next.t("product:Currency"), i18next.t("product:Currency - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.currency} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} onChange={(value => {
this.updateProductField('currency', value); this.updateProductField("currency", value);
})}> })}>
{ {
[ [
{id: 'USD', name: 'USD'}, {id: "USD", name: "USD"},
{id: 'CNY', name: 'CNY'}, {id: "CNY", name: "CNY"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} : {Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.price} onChange={value => { <InputNumber value={this.state.product.price} onChange={value => {
this.updateProductField('price', value); this.updateProductField("price", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} : {Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.quantity} onChange={value => { <InputNumber value={this.state.product.quantity} onChange={value => {
this.updateProductField('quantity', value); this.updateProductField("quantity", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} : {Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.sold} onChange={value => { <InputNumber value={this.state.product.sold} onChange={value => {
this.updateProductField('sold', value); this.updateProductField("sold", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} : {Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.product.providers} onChange={(value => {this.updateProductField('providers', value);})}> <Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}>
{ {
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>) this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Return URL"), i18next.t("product:Return URL - Tooltip"))} : {Setting.getLabel(i18next.t("product:Return URL"), i18next.t("product:Return URL - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.product.returnUrl} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.product.returnUrl} onChange={e => {
this.updateProductField('returnUrl', e.target.value); this.updateProductField("returnUrl", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} : {Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.state} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.product.state} onChange={(value => {
this.updateProductField('state', value); this.updateProductField("state", value);
})}> })}>
{ {
[ [
{id: 'Published', name: 'Published'}, {id: "Published", name: "Published"},
{id: 'Draft', name: 'Draft'}, {id: "Draft", name: "Draft"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col> </Col>
{ {
@ -248,7 +248,7 @@ class ProductEditPage extends React.Component {
} }
</Row> </Row>
</Card> </Card>
) );
} }
renderPreview() { renderPreview() {
@ -264,7 +264,7 @@ class ProductEditPage extends React.Component {
<ProductBuyPage product={this.state.product} /> <ProductBuyPage product={this.state.product} />
</div> </div>
</Col> </Col>
) );
} }
submitProductEdit(willExist) { submitProductEdit(willExist) {
@ -272,19 +272,19 @@ class ProductEditPage extends React.Component {
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product) ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product)
.then((res) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", "Successfully saved");
this.setState({ this.setState({
productName: this.state.product.name, productName: this.state.product.name,
}); });
if (willExist) { if (willExist) {
this.props.history.push(`/products`); this.props.history.push("/products");
} else { } else {
this.props.history.push(`/products/${this.state.product.name}`); this.props.history.push(`/products/${this.state.product.name}`);
} }
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
this.updateProductField('name', this.state.productName); this.updateProductField("name", this.state.productName);
} }
}) })
.catch(error => { .catch(error => {
@ -295,7 +295,7 @@ class ProductEditPage extends React.Component {
deleteProduct() { deleteProduct() {
ProductBackend.deleteProduct(this.state.product) ProductBackend.deleteProduct(this.state.product)
.then(() => { .then(() => {
this.props.history.push(`/products`); this.props.history.push("/products");
}) })
.catch(error => { .catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`); Setting.showMessage("error", `Product failed to delete: ${error}`);
@ -308,10 +308,10 @@ class ProductEditPage extends React.Component {
{ {
this.state.product !== null ? this.renderProduct() : null this.state.product !== null ? this.renderProduct() : null
} }
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>
); );

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from 'antd'; import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as ProductBackend from "./backend/ProductBackend"; import * as ProductBackend from "./backend/ProductBackend";
@ -38,7 +38,7 @@ class ProductListPage extends BaseListPage {
sold: 10, sold: 10,
providers: [], providers: [],
state: "Published", state: "Published",
} };
} }
addProduct() { addProduct() {
@ -56,7 +56,7 @@ class ProductListPage extends BaseListPage {
deleteProduct(i) { deleteProduct(i) {
ProductBackend.deleteProduct(this.state.data[i]) ProductBackend.deleteProduct(this.state.data[i])
.then((res) => { .then((res) => {
Setting.showMessage("success", `Product deleted successfully`); Setting.showMessage("success", "Product deleted successfully");
this.setState({ this.setState({
data: Setting.deleteRow(this.state.data, i), data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1}, pagination: {total: this.state.pagination.total - 1},
@ -72,25 +72,25 @@ class ProductListPage extends BaseListPage {
const columns = [ const columns = [
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
dataIndex: 'name', dataIndex: "name",
key: 'name', key: "name",
width: '140px', width: "140px",
fixed: 'left', fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps('name'), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/products/${text}`}> <Link to={`/products/${text}`}>
{text} {text}
</Link> </Link>
) );
} }
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
dataIndex: 'createdTime', dataIndex: "createdTime",
key: 'createdTime', key: "createdTime",
width: '160px', width: "160px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
@ -98,79 +98,79 @@ class ProductListPage extends BaseListPage {
}, },
{ {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: 'displayName', dataIndex: "displayName",
key: 'displayName', key: "displayName",
width: '170px', width: "170px",
sorter: true, sorter: true,
...this.getColumnSearchProps('displayName'), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: i18next.t("product:Image"), title: i18next.t("product:Image"),
dataIndex: 'image', dataIndex: "image",
key: 'image', key: "image",
width: '170px', width: "170px",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<a target="_blank" rel="noreferrer" href={text}> <a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={150} /> <img src={text} alt={text} width={150} />
</a> </a>
) );
} }
}, },
{ {
title: i18next.t("product:Tag"), title: i18next.t("product:Tag"),
dataIndex: 'tag', dataIndex: "tag",
key: 'tag', key: "tag",
width: '160px', width: "160px",
sorter: true, sorter: true,
...this.getColumnSearchProps('tag'), ...this.getColumnSearchProps("tag"),
}, },
{ {
title: i18next.t("product:Currency"), title: i18next.t("product:Currency"),
dataIndex: 'currency', dataIndex: "currency",
key: 'currency', key: "currency",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('currency'), ...this.getColumnSearchProps("currency"),
}, },
{ {
title: i18next.t("product:Price"), title: i18next.t("product:Price"),
dataIndex: 'price', dataIndex: "price",
key: 'price', key: "price",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('price'), ...this.getColumnSearchProps("price"),
}, },
{ {
title: i18next.t("product:Quantity"), title: i18next.t("product:Quantity"),
dataIndex: 'quantity', dataIndex: "quantity",
key: 'quantity', key: "quantity",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('quantity'), ...this.getColumnSearchProps("quantity"),
}, },
{ {
title: i18next.t("product:Sold"), title: i18next.t("product:Sold"),
dataIndex: 'sold', dataIndex: "sold",
key: 'sold', key: "sold",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('sold'), ...this.getColumnSearchProps("sold"),
}, },
{ {
title: i18next.t("general:State"), title: i18next.t("general:State"),
dataIndex: 'state', dataIndex: "state",
key: 'state', key: "state",
width: '120px', width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps('state'), ...this.getColumnSearchProps("state"),
}, },
{ {
title: i18next.t("product:Payment providers"), title: i18next.t("product:Payment providers"),
dataIndex: 'providers', dataIndex: "providers",
key: 'providers', key: "providers",
width: '500px', width: "500px",
...this.getColumnSearchProps('providers'), ...this.getColumnSearchProps("providers"),
render: (text, record, index) => { render: (text, record, index) => {
const providers = text; const providers = text;
if (providers.length === 0) { if (providers.length === 0) {
@ -197,11 +197,11 @@ class ProductListPage extends BaseListPage {
</Link> </Link>
</div> </div>
</List.Item> </List.Item>
) );
}} }}
/> />
) );
} };
return ( return (
<div> <div>
@ -218,28 +218,28 @@ class ProductListPage extends BaseListPage {
</Col> </Col>
</Row> </Row>
</div> </div>
) );
}, },
}, },
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: "",
key: 'op', key: "op",
width: '230px', width: "230px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} onClick={() => this.props.history.push(`/products/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/products/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm <Popconfirm
title={`Sure to delete product: ${record.name} ?`} title={`Sure to delete product: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)} onConfirm={() => this.deleteProduct(index)}
> >
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button> <Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm> </Popconfirm>
</div> </div>
) );
} }
}, },
]; ];
@ -253,7 +253,7 @@ class ProductListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={products} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={products} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Products")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Products")}&nbsp;&nbsp;&nbsp;&nbsp;

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