Compare commits

...

77 Commits

Author SHA1 Message Date
7e46222ecd Revert "feat: add Casbin editor's checking in model editor (#3166)"
This reverts commit a1b010a406.
2024-09-03 21:58:51 +08:00
a1b010a406 feat: add Casbin editor's checking in model editor (#3166)
* feat: add model syntax linting and update dependencies

* refactor: move model linter logic to separate module
2024-09-03 21:32:45 +08:00
89e92cbd47 feat: when using basic auth to fetch access_token will return restful response to oidc client (#3164) 2024-09-03 08:05:29 +08:00
d4c8193357 feat: support reCAPTCHA v3 captcha provider (#3160)
* feat: support reCAPTCHA v3 captcha provider

* fix: modify the implementation of row component style in CaptchaModal.js
2024-09-02 22:15:03 +08:00
9b33800b4c feat: add email_verified, phone_number and phone_number_verified field for standard jwt token (#3156)
* feat: add email_verified, phone_number and phone_number_verified field for standard jwt token

* fix: fix linter err
2024-08-31 12:49:39 +08:00
ec98785172 feat: certEditPage will be redirected to 404 when name is changed (#3154) 2024-08-30 23:04:50 +08:00
45dd4cc344 feat: fix nonce not parsed issue in fastAutoSignin() (#3153)
* fix: fix nonce none passed when auto sign enabled

* fix: fix query error
2024-08-30 22:29:23 +08:00
1adb172d6b feat: add more crypto algorithm for jwt signing (#3150)
* feat: add more algorithm support for JWT signing

* feat: add i18n support

* feat: add i18n support

* feat: optimize if statement

* fix: remove additional space line
2024-08-30 16:59:41 +08:00
c08f2b1f3f feat: support Casdoor storage provider (#3147)
* feat: support Casdoor storage provider

* fix: fix code format and nil pointer error

* feat: change cert if statement
2024-08-27 23:54:03 +08:00
62bb257c6d feat: make Resource.Url length to 500 2024-08-26 23:57:41 +08:00
230a77e3e3 feat: add captcha page (#3144) 2024-08-26 23:22:53 +08:00
dce0a96dea feat: improve uploaded file URL 2024-08-26 21:41:28 +08:00
65563fa0cd feat: Ensure MFA email and phone are validated before enabling (#3143)
Added validation checks to ensure that a user's email and phone number are provided before enabling MFA email and phone respectively. This fixes the issue where MFA could be enabled without these values, causing inconsistencies.
2024-08-26 08:40:22 +08:00
f2a94f671a feat: complete i18n translation (#3141)
* feat: complete i18n translation

* fix: fix problem in cs/data
2024-08-24 23:27:59 +08:00
1460a0498f feat: support assign a default group for synchronized from external openldap (#3140)
* feat: support default sync group for ldap (with without add i18n translate)

* feat: improve translation

* feat: update all i18n translation

* revert: remove new i18n translation
2024-08-24 00:12:52 +08:00
adc63ea726 feat: fix wrong error alert in ApiFilter's getObject() 2024-08-23 23:36:55 +08:00
0b8be016c5 feat: add enableErrorMask config 2024-08-23 22:19:17 +08:00
986dcbbda1 feat: handle error in ApiFilter 2024-08-23 21:50:48 +08:00
7d3920fb1f feat: add ManagedAccounts to JWT 2024-08-20 22:23:58 +08:00
b794ef87ee feat: Revert "feat: support reCAPTCHA v3 captcha provider" (#3135)
This reverts commit a0d6f2125e.
2024-08-20 17:56:53 +08:00
a0d6f2125e feat: support reCAPTCHA v3 captcha provider (#3130) 2024-08-20 17:29:37 +08:00
85cbb7d074 feat: add replaceAll polyfill to be compatible with Firefox 68 2024-08-17 18:37:21 +08:00
fdc1be9452 feat: add provider.Bucket to fileUrl response and TrimPrefix "/" before delete GCS object (#3129)
* feat: add provider.Bucket to fileUrl response

* feat: TrimPrefix "/" before Google Cloud Storage delete object
2024-08-17 11:46:58 +08:00
2bd7dabd33 feat: allow custom Domain of Google Cloud Storage Provider (#3128) 2024-08-15 23:28:36 +08:00
9b9a58e7ac feat: update casdoor/oss version to support Google Cloud's Application Default Credentials (#3125) 2024-08-15 13:45:27 +08:00
38e389e8c8 feat: Pagination not updating after last item deletion (#3120) 2024-08-13 16:09:16 +08:00
ab5fcf848e feat: support accessKey and accessSecret login in AutoSigninFilter (#3117) 2024-08-12 12:20:41 +08:00
b4e51b4631 feat: improve error message in GetFailedSigninConfigByUser() 2024-08-10 09:31:46 +08:00
45e25acc80 feat: fix JWT generate issue cause by shared application (#3113)
* fix: fix jwt generate cause by shared application

* fix: fix built-in org will not add -org-
2024-08-09 22:48:44 +08:00
97dcf24a91 feat: improve error message in GetAuthorizationCodeToken() 2024-08-09 21:06:23 +08:00
4c0fff66ff feat: support shared application across organizations (#3108)
* feat: support share application

* revert: revert i18n

* fix: improve code format

* fix: improve code format and move GetSharedOrgFromApp to string.go
2024-08-09 15:43:25 +08:00
e7230700e0 feat: Revert "feat: fix Beego session delete concurrent issue" (#3105)
This reverts commit f21aa9c0d2.
2024-08-07 16:51:54 +08:00
f21aa9c0d2 feat: fix Beego session delete concurrent issue (#3103) 2024-08-07 16:29:35 +08:00
4b2b875b2d feat: Czech, Slovak localization (#3095)
* feat: add l10n Czech, Slovak language support

* feat: i18n Czech, Slovak translation
2024-08-02 09:39:47 +08:00
df2a5681cc feat: add missing account items in CheckPermissionForUpdateUser() (#3094) 2024-08-01 23:34:12 +08:00
ac102480c7 feat: support Radius Challenge/Response for MFA (RFC2865) feature request (#3093)
* feat: support RFC2865 for radius server when user enable TOTP mfa

* fix: fix linter err
2024-08-01 22:02:49 +08:00
feff47d2dc feat: skip agreement check when the terms are not visible (#3088) 2024-07-30 14:04:03 +08:00
79b934d6c2 feat: enforce acceptance of terms and conditions for social logins (#3087)
* feat: Enforce acceptance of terms and conditions for social logins (#2975)

* feat: add error message for agreement acceptance
2024-07-29 17:22:48 +08:00
365449695b fix: fix application field in invitationEditPage will use translation of "All" as value (#3085) 2024-07-29 01:35:28 +08:00
55a52093e8 feat: fix bug that user can signup without invitation code via OAuth (#3084)
* fix:fix user can signup without invitation code when using 3rd oauth

* fix:use correct i18n translation
2024-07-29 00:59:02 +08:00
e65fdeb1e0 feat: ABAC support for /api/batch-enforce endpoint (#3082) 2024-07-27 09:43:58 +08:00
a46c1cc775 feat: update WeCom OAuth URLs (#3080) 2024-07-26 22:03:24 +08:00
5629343466 feat: fix missing extendApplicationWithSigninMethods() in getDefaultApplication() (#3076) 2024-07-24 22:30:15 +08:00
3718d2dc04 feat: improve name mapping in LarkIdProvider (#3075)
* fix: change user identifier to the `user_id` field in IdP Lark, and use Chinese name to be the display name

* Update lark.go

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-07-23 21:12:53 +08:00
38b9ad1d9f feat: Add Support for memberOf Overlay in LDAP Server (#3068)
* feat: Allow All Users to Perform LDAP Search Lookups in their org

* feat: add ldap member of support
2024-07-21 01:25:42 +08:00
5a92411006 feat: add MFA accounts table (#3066)
* feat: add mfa accounts store

* fix: change MFA to Mfa

* fix: change MFA to Mfa

* fix: delete api
2024-07-20 22:51:15 +08:00
52eaf6c822 feat: Allow All Users to Perform LDAP Search Lookups in their org (#3064) 2024-07-20 20:44:29 +08:00
cc84709151 feat: add webhook support for invoice-payment and notify-payment (#3062) 2024-07-20 12:49:34 +08:00
22fca78be9 feat: fix bug in AdapterEditPage 2024-07-19 00:57:56 +08:00
DSP
effd257040 feat: fix isPasswordWithLdapEnabled logic in handleBind() for redirecting to other LDAP sources (#3059)
* Added parameters to function call in server.go

Added needed parameters for redirection to other LDAP sources to function correctly and not always run into the "wrong credentials" error

* Update server.go

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-07-18 21:04:17 +08:00
a38747d90e feat: fix bug in GetPolicies() 2024-07-18 18:40:55 +08:00
da70682cd1 feat: fix bug in obtaining Casdoor version in Docker (#3056) 2024-07-16 18:13:44 +08:00
4a3bd84f84 feat: fix the problem of abnormal tour when refreshing (#3054)
* fix: fix the problem of abnormal tour when refreshing

* fix: change the way enableTour configuration is stored
2024-07-12 19:27:55 +08:00
7f2869cecb feat: link transaction with balance and payment (#3052)
* feat: add and update transaction when recharging

* feat: add pay with balance

* feat: improve code format

* feat: update icon url for balance
2024-07-12 15:48:37 +08:00
cef2ab213b feat: add JWT-Standard format to fix oidc address type problem (#3050)
* feat: add JWT-Standard option to return standard OIDC UserInfo

* fix: fix error occurs by different claim type

* feat: improve code format and add missing return
2024-07-12 09:36:50 +08:00
cc979c310e feat: OAuth provider lark supports getting phone number (#3047) 2024-07-11 08:56:28 +08:00
13d73732ce fix: improve initBuiltInOrganization() 2024-07-10 14:18:30 +08:00
5686fe5d22 feat: use orgnization logo as tour logo and allow to configure whether to enable tour in organization edit page (#3046) 2024-07-10 14:18:04 +08:00
d8cb82f67a feat: upgrade CI Node.js version to 20 2024-07-09 13:09:40 +08:00
cad2e1bcc3 feat: don't drop empty table for adapters (#3043)
* fix: solve the problem of update operation returning 'unaffected'

* feat: remove the action for Dropping empty adapter data table
2024-07-09 11:35:22 +08:00
52cc2e4fa7 feat: fix bug in permission's owner edit (#3041) 2024-07-06 11:24:08 +08:00
8077a2ccba feat: fix bug for access key and secret login (#3022)
* fix: get username for keys

* chore: move user nil check
2024-06-27 21:24:54 +08:00
4cb8e4a514 feat: Revert "feat: fix OIDC address field" (#3020)
This reverts commit 2f48d45773.
2024-06-25 16:14:26 +08:00
2f48d45773 feat: fix OIDC address field (#3013)
* feat:add fields of sync-database

* feat:add fields of sync-database

* feat: add several fields related to the OIDC specification address

* feat: add the field Address to Address structure in UserWithoutThirdIdp

* fix: delete redundant fields

* fix: add Address struct and delete redundant fields
2024-06-25 11:54:34 +08:00
cff0c7a273 feat: support "Use Email as username" in org (#3002)
Signed-off-by: Grégoire Bélorgey <gregoire@jianda.fr>
2024-06-22 16:52:11 +08:00
793a7d6cda feat: add free charge price mode for product buy page (#3015)
* feat: add free charge price mode for product buy page

* fix: improve code format
2024-06-22 14:05:53 +08:00
4cc2120fed feat: fix the top Navbar UI is broken issue (#3000) 2024-06-09 17:05:04 +08:00
93b0f52f26 feat: Revert "feat: fix cannot create "/files" folder issue in local file storage provider in Docker" (#2997)
This reverts commit e228045e37.
2024-06-06 11:09:02 +08:00
e228045e37 feat: fix cannot create "/files" folder issue in local file storage provider in Docker (#2994) 2024-06-06 10:49:56 +08:00
6b8c24e1f0 feat: fix password not encrypted issue in SetPassword() API (#2990)
* fix: fix password not encrypted in set password and password type not changed

* Update user.go

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-06-04 13:32:13 +08:00
8a79bb64dd feat: test SMTP connection with browser parameters (#2986) 2024-06-04 01:34:36 +08:00
e5f9aab28f feat: support resetting password on first login (#2980)
* feat: support reset password in first login

* feat: disable needUpdatePassword when user haven't email and phone and mfa
2024-06-02 01:00:55 +08:00
7d05b69aac feat: remove useless code 2024-05-28 20:33:55 +08:00
868e66e866 feat: fix QQ login error when using mobile browser (#2971) 2024-05-27 01:07:15 +08:00
40ad3c9234 feat: support MFA fields in syncer (#2966)
* feat:add fields of sync-database

* feat:add fields of sync-database
2024-05-27 01:06:59 +08:00
e2cd0604c2 feat: add back arm64 support in Docker image (#2969) 2024-05-26 01:22:49 +08:00
78c3065fbb feat: fix address field bug in user edit page 2024-05-24 17:19:27 +08:00
138 changed files with 5237 additions and 355 deletions

View File

@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
cache: 'yarn' cache: 'yarn'
cache-dependency-path: ./web/yarn.lock cache-dependency-path: ./web/yarn.lock
- run: yarn install && CI=false yarn run build - run: yarn install && CI=false yarn run build
@ -101,7 +101,7 @@ jobs:
working-directory: ./ working-directory: ./
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
cache: 'yarn' cache: 'yarn'
cache-dependency-path: ./web/yarn.lock cache-dependency-path: ./web/yarn.lock
- run: yarn install - run: yarn install
@ -138,7 +138,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Fetch Previous version - name: Fetch Previous version
id: get-previous-tag id: get-previous-tag
@ -194,7 +194,7 @@ jobs:
with: with:
context: . context: .
target: STANDARD target: STANDARD
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
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
@ -204,7 +204,7 @@ jobs:
with: with:
context: . context: .
target: ALLINONE target: ALLINONE
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: casbin/casdoor-all-in-one:${{steps.get-current-tag.outputs.tag }},casbin/casdoor-all-in-one:latest tags: casbin/casdoor-all-in-one:${{steps.get-current-tag.outputs.tag }},casbin/casdoor-all-in-one:latest

View File

@ -1,10 +1,10 @@
FROM node:18.19.0 AS FRONT FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
WORKDIR /web WORKDIR /web
COPY ./web . COPY ./web .
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
FROM golang:1.20.12 AS BACK FROM --platform=$BUILDPLATFORM golang:1.20.12 AS BACK
WORKDIR /go/src/casdoor WORKDIR /go/src/casdoor
COPY . . COPY . .
RUN ./build.sh RUN ./build.sh
@ -13,6 +13,9 @@ RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go >
FROM alpine:latest AS STANDARD FROM alpine:latest AS STANDARD
LABEL MAINTAINER="https://casdoor.org/" LABEL MAINTAINER="https://casdoor.org/"
ARG USER=casdoor ARG USER=casdoor
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN sed -i 's/https/http/' /etc/apk/repositories RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add --update sudo RUN apk add --update sudo
@ -28,7 +31,7 @@ RUN adduser -D $USER -u 1000 \
USER 1000 USER 1000
WORKDIR / WORKDIR /
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server ./server COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server_${BUILDX_ARCH} ./server
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/swagger ./swagger COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/swagger ./swagger
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/conf/app.conf ./conf/app.conf COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/conf/app.conf ./conf/app.conf
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
@ -47,12 +50,15 @@ RUN apt update \
FROM db AS ALLINONE FROM db AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/" LABEL MAINTAINER="https://casdoor.org/"
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN apt update RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates RUN apt install -y ca-certificates && update-ca-certificates
WORKDIR / WORKDIR /
COPY --from=BACK /go/src/casdoor/server ./server COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server
COPY --from=BACK /go/src/casdoor/swagger ./swagger 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/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf

View File

@ -8,4 +8,6 @@ else
echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct" echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct"
export GOPROXY="https://goproxy.cn,direct" export GOPROXY="https://goproxy.cn,direct"
fi fi
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server_linux_amd64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o server_linux_arm64 .

View File

@ -26,6 +26,10 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
return NewDefaultCaptchaProvider() return NewDefaultCaptchaProvider()
case "reCAPTCHA": case "reCAPTCHA":
return NewReCaptchaProvider() return NewReCaptchaProvider()
case "reCAPTCHA v2":
return NewReCaptchaProvider()
case "reCAPTCHA v3":
return NewReCaptchaProvider()
case "Aliyun Captcha": case "Aliyun Captcha":
return NewAliyunCaptchaProvider() return NewAliyunCaptchaProvider()
case "hCaptcha": case "hCaptcha":

View File

@ -21,6 +21,7 @@ originFrontend =
staticBaseUrl = "https://cdn.casbin.org" staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false isDemoMode = false
batchSize = 100 batchSize = 100
enableErrorMask = false
enableGzip = true enableGzip = true
ldapServerPort = 389 ldapServerPort = 389
radiusServerPort = 1812 radiusServerPort = 1812

View File

@ -169,7 +169,11 @@ func (c *ApiController) Signup() {
username := authForm.Username username := authForm.Username
if !application.IsSignupItemVisible("Username") { if !application.IsSignupItemVisible("Username") {
username = id if organization.UseEmailAsUsername && application.IsSignupItemVisible("Email") {
username = authForm.Email
} else {
username = id
}
} }
initScore, err := organization.GetInitScore() initScore, err := organization.GetInitScore()

View File

@ -117,7 +117,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
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)
resp = &Response{Status: "ok", Msg: "", Data: userId} resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
} else if form.Type == ResponseTypeCode { } else if form.Type == ResponseTypeCode {
clientId := c.Input().Get("clientId") clientId := c.Input().Get("clientId")
responseType := c.Input().Get("responseType") responseType := c.Input().Get("responseType")
@ -139,7 +139,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
} }
resp = codeToResponse(code) resp = codeToResponse(code)
resp.Data2 = user.NeedUpdatePassword
if application.EnableSigninSession || application.HasPromptPage() { if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in // The prompt page needs the user to be signed in
c.SetSessionUsername(userId) c.SetSessionUsername(userId)
@ -152,6 +152,8 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
nonce := c.Input().Get("nonce") nonce := c.Input().Get("nonce")
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host) token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token) resp = tokenToResponse(token)
resp.Data2 = user.NeedUpdatePassword
} }
} else if form.Type == ResponseTypeSaml { // saml flow } else if form.Type == ResponseTypeSaml { // saml flow
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host) res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
@ -159,7 +161,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
c.ResponseError(err.Error(), nil) c.ResponseError(err.Error(), nil)
return return
} }
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]string{"redirectUrl": redirectUrl, "method": method}} resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]interface{}{"redirectUrl": redirectUrl, "method": method, "needUpdatePassword": user.NeedUpdatePassword}}
if application.EnableSigninSession || application.HasPromptPage() { if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in // The prompt page needs the user to be signed in
@ -663,6 +665,11 @@ func (c *ApiController) Login() {
return return
} }
if application.IsSignupItemRequired("Invitation code") {
c.ResponseError(c.T("check:Invitation code cannot be blank"))
return
}
// Handle username conflicts // Handle username conflicts
var tmpUser *object.User var tmpUser *object.User
tmpUser, err = object.GetUser(util.GetId(application.Organization, userInfo.Username)) tmpUser, err = object.GetUser(util.GetId(application.Organization, userInfo.Username))

View File

@ -16,6 +16,7 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -163,11 +164,17 @@ func (c *ApiController) GetPolicies() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if adapter == nil {
c.ResponseError(fmt.Sprintf(c.T("the adapter: %s is not found"), adapterId))
return
}
err = adapter.InitAdapter() err = adapter.InitAdapter()
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk() c.ResponseOk()
return return
} }

View File

@ -60,7 +60,6 @@ func (c *ApiController) Unlink() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if application == nil { if application == nil {
c.ResponseError(c.T("link:You can't unlink yourself, you are not a member of any application")) c.ResponseError(c.T("link:You can't unlink yourself, you are not a member of any application"))
return return

View File

@ -17,6 +17,7 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -164,6 +165,16 @@ func (c *ApiController) BuyProduct() {
host := c.Ctx.Request.Host host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName") providerName := c.Input().Get("providerName")
paymentEnv := c.Input().Get("paymentEnv") paymentEnv := c.Input().Get("paymentEnv")
customPriceStr := c.Input().Get("customPrice")
if customPriceStr == "" {
customPriceStr = "0"
}
customPrice, err := strconv.ParseFloat(customPriceStr, 64)
if err != nil {
c.ResponseError(err.Error())
return
}
// buy `pricingName/planName` for `paidUserName` // buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName") pricingName := c.Input().Get("pricingName")
@ -189,7 +200,7 @@ func (c *ApiController) BuyProduct() {
return return
} }
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv) payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -257,7 +257,7 @@ func (c *ApiController) UploadResource() {
fileType, _ = util.GetOwnerAndNameFromIdNoCheck(mimeType + "/") fileType, _ = util.GetOwnerAndNameFromIdNoCheck(mimeType + "/")
} }
fullFilePath = object.GetTruncatedPath(provider, fullFilePath, 175) fullFilePath = object.GetTruncatedPath(provider, fullFilePath, 450)
if tag != "avatar" && tag != "termsOfUse" && !strings.HasPrefix(tag, "idCard") { if tag != "avatar" && tag != "termsOfUse" && !strings.HasPrefix(tag, "idCard") {
ext := filepath.Ext(filepath.Base(fullFilePath)) ext := filepath.Ext(filepath.Base(fullFilePath))
index := len(fullFilePath) - len(ext) index := len(fullFilePath) - len(ext)

View File

@ -27,11 +27,12 @@ import (
) )
type EmailForm struct { type EmailForm struct {
Title string `json:"title"` Title string `json:"title"`
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"` Provider string `json:"provider"`
ProviderObject object.Provider `json:"providerObject"`
} }
type SmsForm struct { type SmsForm struct {
@ -74,7 +75,6 @@ func (c *ApiController) SendEmail() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
} else { } 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 // 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
provider, err = c.GetProviderFromContext("Email") provider, err = c.GetProviderFromContext("Email")
@ -84,6 +84,13 @@ func (c *ApiController) SendEmail() {
} }
} }
if emailForm.ProviderObject.Name != "" {
if emailForm.ProviderObject.ClientSecret == "***" {
emailForm.ProviderObject.ClientSecret = provider.ClientSecret
}
provider = &emailForm.ProviderObject
}
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email // 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" { if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
err = object.DailSmtpServer(provider) err = object.DailSmtpServer(provider)

View File

@ -46,10 +46,10 @@ func (c *ApiController) GetSystemInfo() {
// @Success 200 {object} util.VersionInfo The Response object // @Success 200 {object} util.VersionInfo The Response object
// @router /get-version-info [get] // @router /get-version-info [get]
func (c *ApiController) GetVersionInfo() { func (c *ApiController) GetVersionInfo() {
errInfo := ""
versionInfo, err := util.GetVersionInfo() versionInfo, err := util.GetVersionInfo()
if err != nil { if err != nil {
c.ResponseError(err.Error()) errInfo = "Git error: " + err.Error()
return
} }
if versionInfo.Version != "" { if versionInfo.Version != "" {
@ -59,9 +59,11 @@ func (c *ApiController) GetVersionInfo() {
versionInfo, err = util.GetVersionInfoFromFile() versionInfo, err = util.GetVersionInfoFromFile()
if err != nil { if err != nil {
c.ResponseError(err.Error()) errInfo = errInfo + ", File error: " + err.Error()
c.ResponseError(errInfo)
return return
} }
c.ResponseOk(versionInfo) c.ResponseOk(versionInfo)
} }

View File

@ -333,6 +333,35 @@ func (c *ApiController) IntrospectToken() {
return return
} }
if application.TokenFormat == "JWT-Standard" {
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil {
// and token revoked case. but we not implement
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
// refs: https://tools.ietf.org/html/rfc7009
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
}
c.Data["json"] = &object.IntrospectionResponse{
Active: true,
Scope: jwtToken.Scope,
ClientId: clientId,
Username: token.User,
TokenType: token.TokenType,
Exp: jwtToken.ExpiresAt.Unix(),
Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(),
Sub: jwtToken.Subject,
Aud: jwtToken.Audience,
Iss: jwtToken.Issuer,
Jti: jwtToken.ID,
}
c.ServeJSON()
return
}
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application) jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil { if err != nil || jwtToken.Valid() != nil {
// and token revoked case. but we not implement // and token revoked case. but we not implement

View File

@ -289,6 +289,16 @@ func (c *ApiController) UpdateUser() {
} }
} }
if user.MfaEmailEnabled && user.Email == "" {
c.ResponseError(c.T("user:MFA email is enabled but email is empty"))
return
}
if user.MfaPhoneEnabled && user.Phone == "" {
c.ResponseError(c.T("user:MFA phone is enabled but phone number is empty"))
return
}
if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" { if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
return return
@ -509,8 +519,21 @@ func (c *ApiController) SetPassword() {
return return
} }
organization, err := object.GetOrganizationByUser(targetUser)
if err != nil {
c.ResponseError(err.Error())
return
}
if organization == nil {
c.ResponseError(fmt.Sprintf(c.T("the organization: %s is not found"), targetUser.Owner))
return
}
targetUser.Password = newPassword targetUser.Password = newPassword
_, err = object.SetUserField(targetUser, "password", targetUser.Password) targetUser.UpdateUserPassword(organization)
targetUser.NeedUpdatePassword = false
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -45,6 +45,13 @@ func (c *ApiController) ResponseOk(data ...interface{}) {
// ResponseError ... // ResponseError ...
func (c *ApiController) ResponseError(error string, data ...interface{}) { func (c *ApiController) ResponseError(error string, data ...interface{}) {
enableErrorMask := conf.GetConfigBool("enableErrorMask")
if enableErrorMask {
if strings.HasPrefix(error, "The user: ") && strings.HasSuffix(error, " doesn't exist") || strings.HasPrefix(error, "用户: ") && strings.HasSuffix(error, "不存在") {
error = c.T("check:password or code is incorrect")
}
}
resp := &Response{Status: "error", Msg: error} resp := &Response{Status: "error", Msg: error}
c.ResponseJsonData(resp, data...) c.ResponseJsonData(resp, data...)
} }

View File

@ -27,7 +27,18 @@ import (
) )
func deployStaticFiles(provider *object.Provider) { func deployStaticFiles(provider *object.Provider) {
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint) certificate := ""
if provider.Category == "Storage" && provider.Type == "Casdoor" {
cert, err := object.GetCert(util.GetId(provider.Owner, provider.Cert))
if err != nil {
panic(err)
}
if cert == nil {
panic(err)
}
certificate = cert.Certificate
}
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint, certificate, provider.Content)
if err != nil { if err != nil {
panic(err) panic(err)
} }

6
go.mod
View File

@ -9,10 +9,10 @@ require (
github.com/beego/beego v1.12.12 github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.77.2 github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.23.0 github.com/casdoor/go-sms-sender v0.24.0
github.com/casdoor/gomail/v2 v2.0.1 github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.45.0 github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.6.0 github.com/casdoor/oss v1.8.0
github.com/casdoor/xorm-adapter/v3 v3.1.0 github.com/casdoor/xorm-adapter/v3 v3.1.0
github.com/casvisor/casvisor-go-sdk v1.4.0 github.com/casvisor/casvisor-go-sdk v1.4.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
@ -30,7 +30,7 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.6.0 github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.4.0 github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/lestrrat-go/jwx v1.2.29 github.com/lestrrat-go/jwx v1.2.29
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9

13
go.sum
View File

@ -1083,16 +1083,18 @@ github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM= github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk= github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5TKnufWZM=
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs= github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc= github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.23.0 h1:N8+By4JNwyilEcx7cp0QGOepafefM88VwV+o3UEFZio= github.com/casdoor/go-sms-sender v0.24.0 h1:LNLsce3EG/87I3JS6UiajF3LlQmdIiCgebEu0IE4wSM=
github.com/casdoor/go-sms-sender v0.23.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0= github.com/casdoor/go-sms-sender v0.24.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w= github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q= github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk= github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ= github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.6.0 h1:IOWrGLJ+VO82qS796eaRnzFPPA1Sn3cotYTi7O/VIlQ= github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
github.com/casdoor/oss v1.6.0/go.mod h1:rJAWA0hLhtu94t6IRpotLUkXO1NWMASirywQYaGizJE= github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk= github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM= github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM=
github.com/casvisor/casvisor-go-sdk v1.4.0 h1:hbZEGGJ1cwdHFAxeXrMoNw6yha6Oyg2F0qQhBNCN/dg= github.com/casvisor/casvisor-go-sdk v1.4.0 h1:hbZEGGJ1cwdHFAxeXrMoNw6yha6Oyg2F0qQhBNCN/dg=
@ -1460,8 +1462,9 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=

View File

@ -45,6 +45,8 @@ func TestGenerateI18nFrontend(t *testing.T) {
applyToOtherLanguage("frontend", "uk", data) applyToOtherLanguage("frontend", "uk", data)
applyToOtherLanguage("frontend", "kk", data) applyToOtherLanguage("frontend", "kk", data)
applyToOtherLanguage("frontend", "fa", data) applyToOtherLanguage("frontend", "fa", data)
applyToOtherLanguage("frontend", "cs", data)
applyToOtherLanguage("frontend", "sk", data)
} }
func TestGenerateI18nBackend(t *testing.T) { func TestGenerateI18nBackend(t *testing.T) {
@ -73,4 +75,6 @@ func TestGenerateI18nBackend(t *testing.T) {
applyToOtherLanguage("backend", "uk", data) applyToOtherLanguage("backend", "uk", data)
applyToOtherLanguage("backend", "kk", data) applyToOtherLanguage("backend", "kk", data)
applyToOtherLanguage("backend", "fa", data) applyToOtherLanguage("backend", "fa", data)
applyToOtherLanguage("backend", "cs", data)
applyToOtherLanguage("backend", "sk", data)
} }

167
i18n/locales/cs/data.json Normal file
View File

@ -0,0 +1,167 @@
{
"account": {
"Failed to add user": "Nepodařilo se přidat uživatele",
"Get init score failed, error: %w": "Nepodařilo se získat počáteční skóre, chyba: %w",
"Please sign out first": "Nejprve se prosím odhlaste",
"The application does not allow to sign up new account": "Aplikace neumožňuje registraci nového účtu"
},
"auth": {
"Challenge method should be S256": "Metoda výzvy by měla být S256",
"Failed to create user, user information is invalid: %s": "Nepodařilo se vytvořit uživatele, informace o uživateli jsou neplatné: %s",
"Failed to login in: %s": "Nepodařilo se přihlásit: %s",
"Invalid token": "Neplatný token",
"State expected: %s, but got: %s": "Očekávaný stav: %s, ale získán: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) neexistuje a není povoleno se registrovat jako nový účet přes %%s, prosím použijte jiný způsob registrace",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) neexistuje a není povoleno se registrovat jako nový účet, prosím kontaktujte svou IT podporu",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) je již propojen s jiným účtem: %s (%s)",
"The application: %s does not exist": "Aplikace: %s neexistuje",
"The login method: login with LDAP is not enabled for the application": "Metoda přihlášení: přihlášení pomocí LDAP není pro aplikaci povolena",
"The login method: login with SMS is not enabled for the application": "Metoda přihlášení: přihlášení pomocí SMS není pro aplikaci povolena",
"The login method: login with email is not enabled for the application": "Metoda přihlášení: přihlášení pomocí emailu není pro aplikaci povolena",
"The login method: login with face is not enabled for the application": "Metoda přihlášení: přihlášení pomocí obličeje není pro aplikaci povolena",
"The login method: login with password is not enabled for the application": "Metoda přihlášení: přihlášení pomocí hesla není pro aplikaci povolena",
"The organization: %s does not exist": "Organizace: %s neexistuje",
"The provider: %s is not enabled for the application": "Poskytovatel: %s není pro aplikaci povolen",
"Unauthorized operation": "Neoprávněná operace",
"Unknown authentication type (not password or provider), form = %s": "Neznámý typ autentizace (není heslo nebo poskytovatel), formulář = %s",
"User's tag: %s is not listed in the application's tags": "Štítek uživatele: %s není uveden v štítcích aplikace",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "Placený uživatel %s nemá aktivní nebo čekající předplatné a aplikace: %s nemá výchozí ceny"
},
"cas": {
"Service %s and %s do not match": "Služba %s a %s se neshodují"
},
"check": {
"Affiliation cannot be blank": "Příslušnost nemůže být prázdná",
"Default code does not match the code's matching rules": "Výchozí kód neodpovídá pravidlům pro shodu kódů",
"DisplayName cannot be blank": "Zobrazované jméno nemůže být prázdné",
"DisplayName is not valid real name": "Zobrazované jméno není platné skutečné jméno",
"Email already exists": "Email již existuje",
"Email cannot be empty": "Email nemůže být prázdný",
"Email is invalid": "Email je neplatný",
"Empty username.": "Prázdné uživatelské jméno.",
"Face data does not exist, cannot log in": "Data obličeje neexistují, nelze se přihlásit",
"Face data mismatch": "Neshoda dat obličeje",
"FirstName cannot be blank": "Křestní jméno nemůže být prázdné",
"Invitation code cannot be blank": "Pozvánkový kód nemůže být prázdný",
"Invitation code exhausted": "Pozvánkový kód vyčerpán",
"Invitation code is invalid": "Pozvánkový kód je neplatný",
"Invitation code suspended": "Pozvánkový kód pozastaven",
"LDAP user name or password incorrect": "Uživatelské jméno nebo heslo LDAP je nesprávné",
"LastName cannot be blank": "Příjmení nemůže být prázdné",
"Multiple accounts with same uid, please check your ldap server": "Více účtů se stejným uid, prosím zkontrolujte svůj ldap server",
"Organization does not exist": "Organizace neexistuje",
"Phone already exists": "Telefon již existuje",
"Phone cannot be empty": "Telefon nemůže být prázdný",
"Phone number is invalid": "Telefonní číslo je neplatné",
"Please register using the email corresponding to the invitation code": "Prosím zaregistrujte se pomocí emailu odpovídajícího pozvánkovému kódu",
"Please register using the phone corresponding to the invitation code": "Prosím zaregistrujte se pomocí telefonu odpovídajícího pozvánkovému kódu",
"Please register using the username corresponding to the invitation code": "Prosím zaregistrujte se pomocí uživatelského jména odpovídajícího pozvánkovému kódu",
"Session outdated, please login again": "Relace je zastaralá, prosím přihlaste se znovu",
"The invitation code has already been used": "Pozvánkový kód již byl použit",
"The user is forbidden to sign in, please contact the administrator": "Uživatel má zakázáno se přihlásit, prosím kontaktujte administrátora",
"The user: %s doesn't exist in LDAP server": "Uživatel: %s neexistuje na LDAP serveru",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Uživatelské jméno může obsahovat pouze alfanumerické znaky, podtržítka nebo pomlčky, nemůže mít po sobě jdoucí pomlčky nebo podtržítka a nemůže začínat nebo končit pomlčkou nebo podtržítkem.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Hodnota \\\"%s\\\" pro pole účtu \\\"%s\\\" neodpovídá regulárnímu výrazu položky účtu",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Hodnota \\\"%s\\\" pro pole registrace \\\"%s\\\" neodpovídá regulárnímu výrazu položky registrace aplikace \\\"%s\\\"",
"Username already exists": "Uživatelské jméno již existuje",
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
"Username is too long (maximum is 39 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 39 znaků).",
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
"password or code is incorrect": "heslo nebo kód je nesprávné",
"password or code is incorrect, you have %d remaining chances": "heslo nebo kód je nesprávné, máte %d zbývajících pokusů",
"unsupported password type: %s": "nepodporovaný typ hesla: %s"
},
"general": {
"Missing parameter": "Chybějící parametr",
"Please login first": "Prosím, přihlaste se nejprve",
"The organization: %s should have one application at least": "Organizace: %s by měla mít alespoň jednu aplikaci",
"The user: %s doesn't exist": "Uživatel: %s neexistuje",
"don't support captchaProvider: ": "nepodporuje captchaProvider: ",
"this operation is not allowed in demo mode": "tato operace není povolena v demo režimu",
"this operation requires administrator to perform": "tato operace vyžaduje administrátora"
},
"ldap": {
"Ldap server exist": "Ldap server existuje"
},
"link": {
"Please link first": "Prosím, nejprve propojte",
"This application has no providers": "Tato aplikace nemá žádné poskytovatele",
"This application has no providers of type": "Tato aplikace nemá žádné poskytovatele typu",
"This provider can't be unlinked": "Tento poskytovatel nemůže být odpojen",
"You are not the global admin, you can't unlink other users": "Nejste globální administrátor, nemůžete odpojovat jiné uživatele",
"You can't unlink yourself, you are not a member of any application": "Nemůžete odpojit sami sebe, nejste členem žádné aplikace"
},
"organization": {
"Only admin can modify the %s.": "Pouze administrátor může upravit %s.",
"The %s is immutable.": "%s je neměnný.",
"Unknown modify rule %s.": "Neznámé pravidlo úpravy %s."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "Oprávnění: \\\"%s\\\" neexistuje"
},
"provider": {
"Invalid application id": "Neplatné ID aplikace",
"the provider: %s does not exist": "poskytovatel: %s neexistuje"
},
"resource": {
"User is nil for tag: avatar": "Uživatel je nil pro tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Uživatelské jméno nebo úplná cesta k souboru je prázdná: uživatelské jméno = %s, úplná cesta k souboru = %s"
},
"saml": {
"Application %s not found": "Aplikace %s nebyla nalezena"
},
"saml_sp": {
"provider %s's category is not SAML": "poskytovatel %s není kategorie SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Prázdné parametry pro emailForm: %v",
"Invalid Email receivers: %s": "Neplatní příjemci emailu: %s",
"Invalid phone receivers: %s": "Neplatní příjemci telefonu: %s"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s není povolen",
"The provider type: %s is not supported": "typ poskytovatele: %s není podporován"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s není v této aplikaci podporován",
"Invalid application or wrong clientSecret": "Neplatná aplikace nebo špatný clientSecret",
"Invalid client_id": "Neplatné client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Přesměrovací URI: %s neexistuje v seznamu povolených přesměrovacích URI",
"Token not found, invalid accessToken": "Token nenalezen, neplatný accessToken"
},
"user": {
"Display name cannot be empty": "Zobrazované jméno nemůže být prázdné",
"New password cannot contain blank space.": "Nové heslo nemůže obsahovat prázdné místo."
},
"user_upload": {
"Failed to import users": "Nepodařilo se importovat uživatele"
},
"util": {
"No application is found for userId: %s": "Pro userId: %s nebyla nalezena žádná aplikace",
"No provider for category: %s is found for application: %s": "Pro kategorii: %s nebyl nalezen žádný poskytovatel pro aplikaci: %s",
"The provider: %s is not found": "Poskytovatel: %s nebyl nalezen"
},
"verification": {
"Invalid captcha provider.": "Neplatný poskytovatel captcha.",
"Phone number is invalid in your region %s": "Telefonní číslo je ve vaší oblasti %s neplatné",
"The verification code has not been sent yet!": "Ověřovací kód ještě nebyl odeslán!",
"The verification code has not been sent yet, or has already been used!": "Ověřovací kód ještě nebyl odeslán, nebo již byl použit!",
"Turing test failed.": "Turingův test selhal.",
"Unable to get the email modify rule.": "Nelze získat pravidlo pro úpravu emailu.",
"Unable to get the phone modify rule.": "Nelze získat pravidlo pro úpravu telefonu.",
"Unknown type": "Neznámý typ",
"Wrong verification code!": "Špatný ověřovací kód!",
"You should verify your code in %d min!": "Měli byste ověřit svůj kód do %d minut!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "prosím přidejte poskytovatele SMS do seznamu \\\"Providers\\\" pro aplikaci: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "prosím přidejte poskytovatele emailu do seznamu \\\"Providers\\\" pro aplikaci: %s",
"the user does not exist, please sign up first": "uživatel neexistuje, prosím nejprve se zaregistrujte"
},
"webauthn": {
"Found no credentials for this user": "Nebyly nalezeny žádné přihlašovací údaje pro tohoto uživatele",
"Please call WebAuthnSigninBegin first": "Prosím, nejprve zavolejte WebAuthnSigninBegin"
}
}

167
i18n/locales/sk/data.json Normal file
View File

@ -0,0 +1,167 @@
{
"account": {
"Failed to add user": "Nepodarilo sa pridať používateľa",
"Get init score failed, error: %w": "Získanie počiatočného skóre zlyhalo, chyba: %w",
"Please sign out first": "Najskôr sa prosím odhláste",
"The application does not allow to sign up new account": "Aplikácia neumožňuje registráciu nového účtu"
},
"auth": {
"Challenge method should be S256": "Metóda výzvy by mala byť S256",
"Failed to create user, user information is invalid: %s": "Nepodarilo sa vytvoriť používateľa, informácie o používateľovi sú neplatné: %s",
"Failed to login in: %s": "Prihlásenie zlyhalo: %s",
"Invalid token": "Neplatný token",
"State expected: %s, but got: %s": "Očakávaný stav: %s, ale dostali sme: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Účet pre poskytovateľa: %s a používateľské meno: %s (%s) neexistuje a nie je povolené zaregistrovať nový účet cez %%s, prosím použite iný spôsob registrácie",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Účet pre poskytovateľa: %s a používateľské meno: %s (%s) neexistuje a nie je povolené zaregistrovať nový účet, prosím kontaktujte vašu IT podporu",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Účet pre poskytovateľa: %s a používateľské meno: %s (%s) je už prepojený s iným účtom: %s (%s)",
"The application: %s does not exist": "Aplikácia: %s neexistuje",
"The login method: login with LDAP is not enabled for the application": "Metóda prihlásenia: prihlásenie pomocou LDAP nie je pre aplikáciu povolená",
"The login method: login with SMS is not enabled for the application": "Metóda prihlásenia: prihlásenie pomocou SMS nie je pre aplikáciu povolená",
"The login method: login with email is not enabled for the application": "Metóda prihlásenia: prihlásenie pomocou e-mailu nie je pre aplikáciu povolená",
"The login method: login with face is not enabled for the application": "Metóda prihlásenia: prihlásenie pomocou tváre nie je pre aplikáciu povolená",
"The login method: login with password is not enabled for the application": "Metóda prihlásenia: prihlásenie pomocou hesla nie je pre aplikáciu povolená",
"The organization: %s does not exist": "Organizácia: %s neexistuje",
"The provider: %s is not enabled for the application": "Poskytovateľ: %s nie je pre aplikáciu povolený",
"Unauthorized operation": "Neautorizovaná operácia",
"Unknown authentication type (not password or provider), form = %s": "Neznámy typ autentifikácie (nie heslo alebo poskytovateľ), forma = %s",
"User's tag: %s is not listed in the application's tags": "Štítok používateľa: %s nie je uvedený v štítkoch aplikácie",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "platiaci používateľ %s nemá aktívne alebo čakajúce predplatné a aplikácia: %s nemá predvolenú cenovú politiku"
},
"cas": {
"Service %s and %s do not match": "Služba %s a %s sa nezhodujú"
},
"check": {
"Affiliation cannot be blank": "Príslušnosť nemôže byť prázdna",
"Default code does not match the code's matching rules": "Predvolený kód nezodpovedá pravidlám zodpovedania kódu",
"DisplayName cannot be blank": "Zobrazované meno nemôže byť prázdne",
"DisplayName is not valid real name": "Zobrazované meno nie je platné skutočné meno",
"Email already exists": "E-mail už existuje",
"Email cannot be empty": "E-mail nemôže byť prázdny",
"Email is invalid": "E-mail je neplatný",
"Empty username.": "Prázdne používateľské meno.",
"Face data does not exist, cannot log in": "Dáta o tvári neexistujú, nemožno sa prihlásiť",
"Face data mismatch": "Nesúlad dát o tvári",
"FirstName cannot be blank": "Meno nemôže byť prázdne",
"Invitation code cannot be blank": "Kód pozvania nemôže byť prázdny",
"Invitation code exhausted": "Kód pozvania bol vyčerpaný",
"Invitation code is invalid": "Kód pozvania je neplatný",
"Invitation code suspended": "Kód pozvania bol pozastavený",
"LDAP user name or password incorrect": "LDAP používateľské meno alebo heslo sú nesprávne",
"LastName cannot be blank": "Priezvisko nemôže byť prázdne",
"Multiple accounts with same uid, please check your ldap server": "Viacero účtov s rovnakým uid, skontrolujte svoj ldap server",
"Organization does not exist": "Organizácia neexistuje",
"Phone already exists": "Telefón už existuje",
"Phone cannot be empty": "Telefón nemôže byť prázdny",
"Phone number is invalid": "Telefónne číslo je neplatné",
"Please register using the email corresponding to the invitation code": "Prosím, zaregistrujte sa pomocou e-mailu zodpovedajúceho kódu pozvania",
"Please register using the phone corresponding to the invitation code": "Prosím, zaregistrujte sa pomocou telefónu zodpovedajúceho kódu pozvania",
"Please register using the username corresponding to the invitation code": "Prosím, zaregistrujte sa pomocou používateľského mena zodpovedajúceho kódu pozvania",
"Session outdated, please login again": "Relácia je zastaraná, prosím, prihláste sa znova",
"The invitation code has already been used": "Kód pozvania už bol použitý",
"The user is forbidden to sign in, please contact the administrator": "Používateľovi je zakázané prihlásenie, prosím, kontaktujte administrátora",
"The user: %s doesn't exist in LDAP server": "Používateľ: %s neexistuje na LDAP serveri",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Používateľské meno môže obsahovať iba alfanumerické znaky, podtržníky alebo pomlčky, nemôže obsahovať po sebe idúce pomlčky alebo podtržníky a nemôže začínať alebo končiť pomlčkou alebo podtržníkom.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Hodnota \\\"%s\\\" pre pole účtu \\\"%s\\\" nezodpovedá regulárnemu výrazu položky účtu",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Hodnota \\\"%s\\\" pre pole registrácie \\\"%s\\\" nezodpovedá regulárnemu výrazu položky registrácie aplikácie \\\"%s\\\"",
"Username already exists": "Používateľské meno už existuje",
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
"Username is too long (maximum is 39 characters).": "Používateľské meno je príliš dlhé (maximum je 39 znakov).",
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
"password or code is incorrect": "heslo alebo kód je nesprávne",
"password or code is incorrect, you have %d remaining chances": "heslo alebo kód je nesprávne, máte %d zostávajúcich pokusov",
"unsupported password type: %s": "nepodporovaný typ hesla: %s"
},
"general": {
"Missing parameter": "Chýbajúci parameter",
"Please login first": "Najskôr sa prosím prihláste",
"The organization: %s should have one application at least": "Organizácia: %s by mala mať aspoň jednu aplikáciu",
"The user: %s doesn't exist": "Používateľ: %s neexistuje",
"don't support captchaProvider: ": "nepodporuje captchaProvider: ",
"this operation is not allowed in demo mode": "táto operácia nie je povolená v demo režime",
"this operation requires administrator to perform": "táto operácia vyžaduje vykonanie administrátorom"
},
"ldap": {
"Ldap server exist": "LDAP server existuje"
},
"link": {
"Please link first": "Najskôr sa prosím prepojte",
"This application has no providers": "Táto aplikácia nemá žiadnych poskytovateľov",
"This application has no providers of type": "Táto aplikácia nemá poskytovateľov typu",
"This provider can't be unlinked": "Tento poskytovateľ nemôže byť odpojený",
"You are not the global admin, you can't unlink other users": "Nie ste globálny administrátor, nemôžete odpojiť iných používateľov",
"You can't unlink yourself, you are not a member of any application": "Nemôžete sa odpojiť, nie ste členom žiadnej aplikácie"
},
"organization": {
"Only admin can modify the %s.": "Len administrátor môže upravovať %s.",
"The %s is immutable.": "%s je nemenný.",
"Unknown modify rule %s.": "Neznáme pravidlo úprav %s."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "Povolenie: \\\"%s\\\" neexistuje"
},
"provider": {
"Invalid application id": "Neplatné id aplikácie",
"the provider: %s does not exist": "poskytovateľ: %s neexistuje"
},
"resource": {
"User is nil for tag: avatar": "Používateľ je nil pre tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Používateľské meno alebo fullFilePath je prázdny: používateľské meno = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Aplikácia %s nebola nájdená"
},
"saml_sp": {
"provider %s's category is not SAML": "kategória poskytovateľa %s nie je SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Prázdne parametre pre emailForm: %v",
"Invalid Email receivers: %s": "Neplatní príjemcovia e-mailu: %s",
"Invalid phone receivers: %s": "Neplatní príjemcovia telefónu: %s"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s nie je povolený",
"The provider type: %s is not supported": "Typ poskytovateľa: %s nie je podporovaný"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s nie je podporovaný v tejto aplikácii",
"Invalid application or wrong clientSecret": "Neplatná aplikácia alebo nesprávny clientSecret",
"Invalid client_id": "Neplatný client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s neexistuje v zozname povolených Redirect URI",
"Token not found, invalid accessToken": "Token nebol nájdený, neplatný accessToken"
},
"user": {
"Display name cannot be empty": "Zobrazované meno nemôže byť prázdne",
"New password cannot contain blank space.": "Nové heslo nemôže obsahovať medzery."
},
"user_upload": {
"Failed to import users": "Nepodarilo sa importovať používateľov"
},
"util": {
"No application is found for userId: %s": "Nebola nájdená žiadna aplikácia pre userId: %s",
"No provider for category: %s is found for application: %s": "Pre aplikáciu: %s nebol nájdený žiadny poskytovateľ pre kategóriu: %s",
"The provider: %s is not found": "Poskytovateľ: %s nebol nájdený"
},
"verification": {
"Invalid captcha provider.": "Neplatný captcha poskytovateľ.",
"Phone number is invalid in your region %s": "Telefónne číslo je neplatné vo vašom regióne %s",
"The verification code has not been sent yet!": "Overovací kód ešte nebol odoslaný!",
"The verification code has not been sent yet, or has already been used!": "Overovací kód ešte nebol odoslaný, alebo bol už použitý!",
"Turing test failed.": "Test Turinga zlyhal.",
"Unable to get the email modify rule.": "Nepodarilo sa získať pravidlo úpravy e-mailu.",
"Unable to get the phone modify rule.": "Nepodarilo sa získať pravidlo úpravy telefónu.",
"Unknown type": "Neznámy typ",
"Wrong verification code!": "Nesprávny overovací kód!",
"You should verify your code in %d min!": "Overte svoj kód za %d minút!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "prosím pridajte SMS poskytovateľa do zoznamu \\\"Poskytovatelia\\\" pre aplikáciu: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "prosím pridajte e-mailového poskytovateľa do zoznamu \\\"Poskytovatelia\\\" pre aplikáciu: %s",
"the user does not exist, please sign up first": "používateľ neexistuje, prosím, zaregistrujte sa najskôr"
},
"webauthn": {
"Found no credentials for this user": "Nenašli sa žiadne prihlasovacie údaje pre tohto používateľa",
"Please call WebAuthnSigninBegin first": "Najskôr prosím zavolajte WebAuthnSigninBegin"
}
}

View File

@ -22,6 +22,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/nyaruka/phonenumbers"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -199,12 +200,25 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
return nil, err return nil, err
} }
var phoneNumber string
var countryCode string
if len(larkUserInfo.Data.Mobile) != 0 {
phoneNumberParsed, err := phonenumbers.Parse(larkUserInfo.Data.Mobile, "")
if err != nil {
return nil, err
}
countryCode = phonenumbers.GetRegionCodeForNumber(phoneNumberParsed)
phoneNumber = fmt.Sprintf("%d", phoneNumberParsed.GetNationalNumber())
}
userInfo := UserInfo{ userInfo := UserInfo{
Id: larkUserInfo.Data.OpenId, Id: larkUserInfo.Data.OpenId,
DisplayName: larkUserInfo.Data.EnName, DisplayName: larkUserInfo.Data.Name,
Username: larkUserInfo.Data.Name, Username: larkUserInfo.Data.UserId,
Email: larkUserInfo.Data.Email, Email: larkUserInfo.Data.Email,
AvatarUrl: larkUserInfo.Data.AvatarUrl, AvatarUrl: larkUserInfo.Data.AvatarUrl,
Phone: phoneNumber,
CountryCode: countryCode,
} }
return &userInfo, nil return &userInfo, nil
} }

View File

@ -35,7 +35,9 @@
"FI", "FI",
"SE", "SE",
"UA", "UA",
"KZ" "KZ",
"CZ",
"SK"
], ],
"defaultAvatar": "", "defaultAvatar": "",
"defaultApplication": "", "defaultApplication": "",
@ -62,7 +64,9 @@
"sv", "sv",
"uk", "uk",
"kk", "kk",
"fa" "fa",
"cs",
"sk"
], ],
"masterPassword": "", "masterPassword": "",
"defaultPassword": "", "defaultPassword": "",

View File

@ -59,7 +59,15 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
} }
bindPassword := string(r.AuthenticationSimple()) bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
enableCaptcha := false
isSigninViaLdap := false
isPasswordWithLdapEnabled := false
if bindPassword != "" {
isPasswordWithLdapEnabled = true
}
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en", enableCaptcha, isSigninViaLdap, isPasswordWithLdapEnabled)
if err != nil { if err != nil {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err) log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
res.SetResultCode(ldap.LDAPResultInvalidCredentials) res.SetResultCode(ldap.LDAPResultInvalidCredentials)
@ -122,6 +130,9 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name)) e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
e.AddAttribute("cn", message.AttributeValue(user.Name)) e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Id)) e.AddAttribute("uid", message.AttributeValue(user.Id))
for _, group := range user.Groups {
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
}
attrs := r.Attributes() attrs := r.Attributes()
for _, attr := range attrs { for _, attr := range attrs {
if string(attr) == "*" { if string(attr) == "*" {

View File

@ -79,6 +79,8 @@ var ldapAttributesMapping = map[string]FieldRelation{
}, },
} }
const ldapMemberOfAttr = "memberOf"
var AdditionalLdapAttributes []message.LDAPString var AdditionalLdapAttributes []message.LDAPString
func init() { func init() {
@ -180,7 +182,22 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
} }
return builder.Not{cond}, nil return builder.Not{cond}, nil
case message.FilterEqualityMatch: case message.FilterEqualityMatch:
field, err := getUserFieldFromAttribute(string(f.AttributeDesc())) attr := string(f.AttributeDesc())
if attr == ldapMemberOfAttr {
groupId := string(f.AssertionValue())
users, err := object.GetGroupUsers(groupId)
if err != nil {
return nil, err
}
var names []string
for _, user := range users {
names = append(names, user.Name)
}
return builder.In("name", names), nil
}
field, err := getUserFieldFromAttribute(attr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -246,7 +263,7 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
return nil, code return nil, code
} }
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org' if name == "*" { // get all users from organization 'org'
if m.Client.IsGlobalAdmin && org == "*" { if m.Client.IsGlobalAdmin && org == "*" {
filteredUsers, err = object.GetGlobalUsersWithFilter(buildSafeCondition(r.Filter())) filteredUsers, err = object.GetGlobalUsersWithFilter(buildSafeCondition(r.Filter()))
if err != nil { if err != nil {

View File

@ -91,11 +91,13 @@ type Application struct {
CertPublicKey string `xorm:"-" json:"certPublicKey"` CertPublicKey string `xorm:"-" json:"certPublicKey"`
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"` SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
IsShared bool `json:"isShared"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"` RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"` TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"` TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
ExpireInHours int `json:"expireInHours"` ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"` RefreshExpireInHours int `json:"refreshExpireInHours"`
@ -123,9 +125,9 @@ func GetApplicationCount(owner, field, value string) (int64, error) {
return session.Count(&Application{}) return session.Count(&Application{})
} }
func GetOrganizationApplicationCount(owner, Organization, field, value string) (int64, error) { func GetOrganizationApplicationCount(owner, organization, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Application{Organization: Organization}) return session.Where("organization = ? or is_shared = ? ", organization, true).Count(&Application{})
} }
func GetApplications(owner string) ([]*Application, error) { func GetApplications(owner string) ([]*Application, error) {
@ -140,7 +142,7 @@ func GetApplications(owner string) ([]*Application, error) {
func GetOrganizationApplications(owner string, organization string) ([]*Application, error) { func GetOrganizationApplications(owner string, organization string) ([]*Application, error) {
applications := []*Application{} applications := []*Application{}
err := ormer.Engine.Desc("created_time").Find(&applications, &Application{Organization: organization}) err := ormer.Engine.Desc("created_time").Where("organization = ? or is_shared = ? ", organization, true).Find(&applications, &Application{})
if err != nil { if err != nil {
return applications, err return applications, err
} }
@ -162,7 +164,7 @@ func GetPaginationApplications(owner string, offset, limit int, field, value, so
func GetPaginationOrganizationApplications(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) ([]*Application, error) { func GetPaginationOrganizationApplications(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) ([]*Application, error) {
applications := []*Application{} applications := []*Application{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder) session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&applications, &Application{Organization: organization}) err := session.Where("organization = ? or is_shared = ? ", organization, true).Find(&applications, &Application{})
if err != nil { if err != nil {
return applications, err return applications, err
} }
@ -337,12 +339,18 @@ func getApplication(owner string, name string) (*Application, error) {
return nil, nil return nil, nil
} }
application := Application{Owner: owner, Name: name} realApplicationName, sharedOrg := util.GetSharedOrgFromApp(name)
application := Application{Owner: owner, Name: realApplicationName}
existed, err := ormer.Engine.Get(&application) existed, err := ormer.Engine.Get(&application)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if application.IsShared && sharedOrg != "" {
application.Organization = sharedOrg
}
if existed { if existed {
err = extendApplicationWithProviders(&application) err = extendApplicationWithProviders(&application)
if err != nil { if err != nil {
@ -428,11 +436,18 @@ func GetApplicationByUserId(userId string) (application *Application, err error)
func GetApplicationByClientId(clientId string) (*Application, error) { func GetApplicationByClientId(clientId string) (*Application, error) {
application := Application{} application := Application{}
existed, err := ormer.Engine.Where("client_id=?", clientId).Get(&application)
realClientId, sharedOrg := util.GetSharedOrgFromApp(clientId)
existed, err := ormer.Engine.Where("client_id=?", realClientId).Get(&application)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if application.IsShared && sharedOrg != "" {
application.Organization = sharedOrg
}
if existed { if existed {
err = extendApplicationWithProviders(&application) err = extendApplicationWithProviders(&application)
if err != nil { if err != nil {
@ -626,6 +641,10 @@ func UpdateApplication(id string, application *Application) (bool, error) {
return false, err return false, err
} }
if application.IsShared == true && application.Organization != "built-in" {
return false, fmt.Errorf("only applications belonging to built-in organization can be shared")
}
for _, providerItem := range application.Providers { for _, providerItem := range application.Providers {
providerItem.Provider = nil providerItem.Provider = nil
} }

View File

@ -52,6 +52,9 @@ func GetFailedSigninConfigByUser(user *User) (int, int, error) {
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
if application == nil {
return 0, 0, fmt.Errorf("the application for user %s is not found", user.GetId())
}
failedSigninLimit := application.FailedSigninLimit failedSigninLimit := application.FailedSigninLimit
if failedSigninLimit == 0 { if failedSigninLimit == 0 {

View File

@ -78,6 +78,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
} }
} }
@ -108,6 +109,8 @@ func initBuiltInOrganization() bool {
AccountItems: getBuiltInAccountItems(), AccountItems: getBuiltInAccountItems(),
EnableSoftDeletion: false, EnableSoftDeletion: false,
IsProfilePublic: false, IsProfilePublic: false,
UseEmailAsUsername: false,
EnableTour: true,
} }
_, err = AddOrganization(organization) _, err = AddOrganization(organization)
if err != nil { if err != nil {

View File

@ -32,6 +32,7 @@ type Ldap struct {
BaseDn string `xorm:"varchar(100)" json:"baseDn"` BaseDn string `xorm:"varchar(100)" json:"baseDn"`
Filter string `xorm:"varchar(200)" json:"filter"` Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"` FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
AutoSync int `json:"autoSync"` AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"` LastSync string `xorm:"varchar(100)" json:"lastSync"`
@ -148,7 +149,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
} }
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host", affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync").Update(ldap) "port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group").Update(ldap)
if err != nil { if err != nil {
return false, nil return false, nil
} }

View File

@ -339,6 +339,10 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
Ldap: syncUser.Uuid, Ldap: syncUser.Uuid,
} }
if ldap.DefaultGroup != "" {
newUser.Groups = []string{ldap.DefaultGroup}
}
affected, err := AddUser(newUser) affected, err := AddUser(newUser)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -112,7 +112,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
ResponseModesSupported: []string{"query", "fragment", "login", "code", "link"}, ResponseModesSupported: []string{"query", "fragment", "login", "code", "link"},
GrantTypesSupported: []string{"password", "authorization_code"}, GrantTypesSupported: []string{"password", "authorization_code"},
SubjectTypesSupported: []string{"public"}, SubjectTypesSupported: []string{"public"},
IdTokenSigningAlgValuesSupported: []string{"RS256"}, IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},
ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"}, ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"},
ClaimsSupported: []string{"iss", "ver", "sub", "aud", "iat", "exp", "id", "type", "displayName", "avatar", "permanentAvatar", "email", "phone", "location", "affiliation", "title", "homepage", "bio", "tag", "region", "language", "score", "ranking", "isOnline", "isAdmin", "isForbidden", "signupApplication", "ldap"}, ClaimsSupported: []string{"iss", "ver", "sub", "aud", "iat", "exp", "id", "type", "displayName", "avatar", "permanentAvatar", "email", "phone", "location", "affiliation", "title", "homepage", "bio", "tag", "region", "language", "score", "ranking", "isOnline", "isAdmin", "isForbidden", "signupApplication", "ldap"},
RequestParameterSupported: true, RequestParameterSupported: true,

View File

@ -72,6 +72,8 @@ type Organization struct {
InitScore int `json:"initScore"` InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"` EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"` IsProfilePublic bool `json:"isProfilePublic"`
UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"` AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
@ -317,6 +319,7 @@ func GetDefaultApplication(id string) (*Application, error) {
if defaultApplication == nil { if defaultApplication == nil {
return nil, fmt.Errorf("The default application: %s does not exist", organization.DefaultApplication) return nil, fmt.Errorf("The default application: %s does not exist", organization.DefaultApplication)
} else { } else {
defaultApplication.Organization = organization.Name
return defaultApplication, nil return defaultApplication, nil
} }
} }
@ -354,6 +357,11 @@ func GetDefaultApplication(id string) (*Application, error) {
return nil, err return nil, err
} }
err = extendApplicationWithSigninMethods(defaultApplication)
if err != nil {
return nil, err
}
return defaultApplication, nil return defaultApplication, nil
} }

View File

@ -39,6 +39,8 @@ type Payment struct {
Currency string `xorm:"varchar(100)" json:"currency"` Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"` Price float64 `json:"price"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
IsRecharge bool `xorm:"bool" json:"isRecharge"`
// Payer Info // Payer Info
User string `xorm:"varchar(100)" json:"user"` User string `xorm:"varchar(100)" json:"user"`
PersonName string `xorm:"varchar(100)" json:"personName"` PersonName string `xorm:"varchar(100)" json:"personName"`
@ -193,11 +195,16 @@ func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp
return payment, nil, err return payment, nil, err
} }
if notifyResult.Price != product.Price { if notifyResult.Price != product.Price && !product.IsRecharge {
err = fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", notifyResult.Price, product.Price) err = fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", notifyResult.Price, product.Price)
return payment, nil, err return payment, nil, err
} }
if payment.IsRecharge {
err = UpdateUserBalance(payment.Owner, payment.User, payment.Price)
return payment, notifyResult, err
}
return payment, notifyResult, nil return payment, notifyResult, nil
} }
@ -215,6 +222,19 @@ func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
transaction, err := GetTransaction(payment.GetId())
if err != nil {
return nil, err
}
if transaction != nil {
transaction.State = payment.State
_, err = UpdateTransaction(transaction.GetId(), transaction)
if err != nil {
return nil, err
}
}
} }
return payment, nil return payment, nil

View File

@ -181,15 +181,15 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
return false, err return false, err
} }
if oldPermission.Adapter != "" && oldPermission.Adapter != permission.Adapter { // if oldPermission.Adapter != "" && oldPermission.Adapter != permission.Adapter {
isEmpty, _ := ormer.Engine.IsTableEmpty(oldPermission.Adapter) // isEmpty, _ := ormer.Engine.IsTableEmpty(oldPermission.Adapter)
if isEmpty { // if isEmpty {
err = ormer.Engine.DropTables(oldPermission.Adapter) // err = ormer.Engine.DropTables(oldPermission.Adapter)
if err != nil { // if err != nil {
return false, err // return false, err
} // }
} // }
} // }
err = addGroupingPolicies(permission) err = addGroupingPolicies(permission)
if err != nil { if err != nil {
@ -312,15 +312,15 @@ func DeletePermission(permission *Permission) (bool, error) {
return false, err return false, err
} }
if permission.Adapter != "" && permission.Adapter != "permission_rule" { // if permission.Adapter != "" && permission.Adapter != "permission_rule" {
isEmpty, _ := ormer.Engine.IsTableEmpty(permission.Adapter) // isEmpty, _ := ormer.Engine.IsTableEmpty(permission.Adapter)
if isEmpty { // if isEmpty {
err = ormer.Engine.DropTables(permission.Adapter) // err = ormer.Engine.DropTables(permission.Adapter)
if err != nil { // if err != nil {
return false, err // return false, err
} // }
} // }
} // }
} }
return affected, nil return affected, nil

View File

@ -39,6 +39,7 @@ type Product struct {
Price float64 `json:"price"` Price float64 `json:"price"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
Sold int `json:"sold"` Sold int `json:"sold"`
IsRecharge bool `json:"isRecharge"`
Providers []string `xorm:"varchar(255)" json:"providers"` Providers []string `xorm:"varchar(255)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
@ -160,7 +161,7 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
return provider, nil return provider, nil
} }
func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string) (payment *Payment, attachInfo map[string]interface{}, err error) { func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string, customPrice float64) (payment *Payment, attachInfo map[string]interface{}, err error) {
product, err := GetProduct(id) product, err := GetProduct(id)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -169,6 +170,14 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
return nil, nil, fmt.Errorf("the product: %s does not exist", id) return nil, nil, fmt.Errorf("the product: %s does not exist", id)
} }
if product.IsRecharge {
if customPrice <= 0 {
return nil, nil, fmt.Errorf("the custom price should bigger than zero")
} else {
product.Price = customPrice
}
}
provider, err := product.getProvider(providerName) provider, err := product.getProvider(providerName)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -218,13 +227,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
NotifyUrl: notifyUrl, NotifyUrl: notifyUrl,
PaymentEnv: paymentEnv, PaymentEnv: paymentEnv,
} }
// custom process for WeChat & WeChat Pay // custom process for WeChat & WeChat Pay
if provider.Type == "WeChat Pay" { if provider.Type == "WeChat Pay" {
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2)) payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
} else if provider.Type == "Balance" {
payReq.PayerId = user.GetId()
} }
payResp, err := pProvider.Pay(payReq) payResp, err := pProvider.Pay(payReq)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -246,6 +259,7 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
Currency: product.Currency, Currency: product.Currency,
Price: product.Price, Price: product.Price,
ReturnUrl: product.ReturnUrl, ReturnUrl: product.ReturnUrl,
IsRecharge: product.IsRecharge,
User: user.Name, User: user.Name,
PayUrl: payResp.PayUrl, PayUrl: payResp.PayUrl,
@ -254,8 +268,46 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
OutOrderId: payResp.OrderId, OutOrderId: payResp.OrderId,
} }
transaction := &Transaction{
Owner: payment.Owner,
Name: payment.Name,
DisplayName: payment.DisplayName,
Provider: provider.Name,
Category: provider.Category,
Type: provider.Type,
ProductName: product.Name,
ProductDisplayName: product.DisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,
Amount: payment.Price,
ReturnUrl: payment.ReturnUrl,
User: payment.User,
Application: owner,
Payment: payment.GetId(),
State: pp.PaymentStateCreated,
}
if provider.Type == "Dummy" { if provider.Type == "Dummy" {
payment.State = pp.PaymentStatePaid payment.State = pp.PaymentStatePaid
err = UpdateUserBalance(user.Owner, user.Name, payment.Price)
if err != nil {
return nil, nil, err
}
} else if provider.Type == "Balance" {
if product.Price > user.Balance {
return nil, nil, fmt.Errorf("insufficient user balance")
}
transaction.Amount = -transaction.Amount
err = UpdateUserBalance(user.Owner, user.Name, -product.Price)
if err != nil {
return nil, nil, err
}
payment.State = pp.PaymentStatePaid
transaction.State = pp.PaymentStatePaid
} }
affected, err := AddPayment(payment) affected, err := AddPayment(payment)
@ -266,6 +318,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if !affected { if !affected {
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment)) return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
} }
if product.IsRecharge || provider.Type == "Balance" {
affected, err = AddTransaction(transaction)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(payment))
}
}
return payment, payResp.AttachInfo, nil return payment, payResp.AttachInfo, nil
} }
@ -304,8 +367,9 @@ func CreateProductForPlan(plan *Plan) *Product {
Price: plan.Price, Price: plan.Price,
Currency: plan.Currency, Currency: plan.Currency,
Quantity: 999, Quantity: 999,
Sold: 0, Sold: 0,
IsRecharge: false,
Providers: plan.PaymentProviders, Providers: plan.PaymentProviders,
State: "Published", State: "Published",

View File

@ -309,6 +309,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
return nil, err return nil, err
} }
return pp, nil return pp, nil
} else if typ == "Balance" {
pp, err := pp.NewBalancePaymentProvider()
if err != nil {
return nil, err
}
return pp, nil
} else { } else {
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type) return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
} }

View File

@ -36,7 +36,7 @@ type Resource struct {
FileType string `xorm:"varchar(100)" json:"fileType"` FileType string `xorm:"varchar(100)" json:"fileType"`
FileFormat string `xorm:"varchar(100)" json:"fileFormat"` FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
FileSize int `json:"fileSize"` FileSize int `json:"fileSize"`
Url string `xorm:"varchar(255)" json:"url"` Url string `xorm:"varchar(500)" json:"url"`
Description string `xorm:"varchar(255)" json:"description"` Description string `xorm:"varchar(255)" json:"description"`
} }

View File

@ -48,7 +48,7 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
if provider.AppId != "" { if provider.AppId != "" {
phoneNumbers = append([]string{provider.AppId}, phoneNumbers...) phoneNumbers = append([]string{provider.AppId}, phoneNumbers...)
} }
} else if provider.Type == sender.Aliyun || provider.Type == sender.SendCloud { } else if provider.Type == sender.Aliyun {
for i, number := range phoneNumbers { for i, number := range phoneNumbers {
phoneNumbers[i] = strings.TrimPrefix(number, "+86") phoneNumbers[i] = strings.TrimPrefix(number, "+86")
} }

View File

@ -30,6 +30,13 @@ import (
var isCloudIntranet bool var isCloudIntranet bool
const (
ProviderTypeGoogleCloudStorage = "Google Cloud Storage"
ProviderTypeTencentCloudCOS = "Tencent Cloud COS"
ProviderTypeAzureBlob = "Azure Blob"
ProviderTypeLocalFileSystem = "Local File System"
)
func init() { func init() {
isCloudIntranet = conf.GetConfigBool("isCloudIntranet") isCloudIntranet = conf.GetConfigBool("isCloudIntranet")
} }
@ -80,27 +87,28 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath) objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
host := "" host := ""
if provider.Type != "Local File System" { if provider.Type != ProviderTypeLocalFileSystem {
// provider.Domain = "https://cdn.casbin.com/casdoor/" // provider.Domain = "https://cdn.casbin.com/casdoor/"
host = util.GetUrlHost(provider.Domain) host = util.GetUrlHost(provider.Domain)
} else { } else {
// provider.Domain = "http://localhost:8000" or "https://door.casdoor.com" // provider.Domain = "http://localhost:8000" or "https://door.casdoor.com"
host = util.UrlJoin(provider.Domain, "/files") host = util.UrlJoin(provider.Domain, "/files")
} }
if provider.Type == "Azure Blob" { if provider.Type == ProviderTypeAzureBlob || provider.Type == ProviderTypeGoogleCloudStorage {
host = util.UrlJoin(host, provider.Bucket) host = util.UrlJoin(host, provider.Bucket)
} }
fileUrl := "" fileUrl := ""
if host != "" { if host != "" {
fileUrl = util.UrlJoin(host, escapePath(objectKey)) // fileUrl = util.UrlJoin(host, escapePath(objectKey))
fileUrl = util.UrlJoin(host, objectKey)
} }
if fileUrl != "" && hasTimestamp { // if fileUrl != "" && hasTimestamp {
fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime()) // fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime())
} // }
if provider.Type == "Tencent Cloud COS" { if provider.Type == ProviderTypeTencentCloudCOS {
objectKey = escapePath(objectKey) objectKey = escapePath(objectKey)
} }
@ -109,7 +117,18 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface, error) { func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface, error) {
endpoint := getProviderEndpoint(provider) endpoint := getProviderEndpoint(provider)
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint) certificate := ""
if provider.Category == "Storage" && provider.Type == "Casdoor" {
cert, err := GetCert(util.GetId(provider.Owner, provider.Cert))
if err != nil {
return nil, err
}
if cert == nil {
return nil, fmt.Errorf("no cert for %s", provider.Cert)
}
certificate = cert.Certificate
}
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint, certificate, provider.Content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -135,17 +154,17 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
} }
fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true) fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true)
objectKeyRefined := refineObjectKey(provider, objectKey)
objectKeyRefined := objectKey object, err := storageProvider.Put(objectKeyRefined, fileBuffer)
if provider.Type == "Google Cloud Storage" {
objectKeyRefined = strings.TrimPrefix(objectKeyRefined, "/")
}
_, err = storageProvider.Put(objectKeyRefined, fileBuffer)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
if provider.Type == "Casdoor" {
fileUrl = object.Path
}
return fileUrl, objectKey, nil return fileUrl, objectKey, nil
} }
@ -184,5 +203,13 @@ func DeleteFile(provider *Provider, objectKey string, lang string) error {
return err return err
} }
return storageProvider.Delete(objectKey) objectKeyRefined := refineObjectKey(provider, objectKey)
return storageProvider.Delete(objectKeyRefined)
}
func refineObjectKey(provider *Provider, objectKey string) string {
if provider.Type == ProviderTypeGoogleCloudStorage {
return strings.TrimPrefix(objectKey, "/")
}
return objectKey
} }

View File

@ -169,6 +169,12 @@ func (syncer *Syncer) setUserByKeyValue(user *User, key string, value string) {
user.TotpSecret = value user.TotpSecret = value
case "SignupApplication": case "SignupApplication":
user.SignupApplication = value user.SignupApplication = value
case "MfaPhoneEnabled":
user.MfaPhoneEnabled = util.ParseBool(value)
case "MfaEmailEnabled":
user.MfaEmailEnabled = util.ParseBool(value)
case "RecoveryCodes":
user.RecoveryCodes = strings.Split(value, ",")
} }
} }
@ -303,6 +309,9 @@ func (syncer *Syncer) getMapFromOriginalUser(user *OriginalUser) map[string]stri
m["PreferredMfaType"] = user.PreferredMfaType m["PreferredMfaType"] = user.PreferredMfaType
m["TotpSecret"] = user.TotpSecret m["TotpSecret"] = user.TotpSecret
m["SignupApplication"] = user.SignupApplication m["SignupApplication"] = user.SignupApplication
m["MfaPhoneEnabled"] = util.BoolToString(user.MfaPhoneEnabled)
m["MfaEmailEnabled"] = util.BoolToString(user.MfaEmailEnabled)
m["RecoveryCodes"] = strings.Join(user.RecoveryCodes, ",")
m2 := map[string]string{} m2 := map[string]string{}
for _, tableColumn := range syncer.TableColumns { for _, tableColumn := range syncer.TableColumns {

View File

@ -277,7 +277,6 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
if application == nil { if application == nil {
return "", "", fmt.Errorf("the application for user %s is not found", userId) return "", "", fmt.Errorf("the application for user %s is not found", userId)
} }

View File

@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"time" "time"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -128,7 +129,7 @@ type UserWithoutThirdIdp struct {
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"` SigninWrongTimes int `json:"signinWrongTimes"`
// ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
} }
type ClaimsShort struct { type ClaimsShort struct {
@ -139,6 +140,15 @@ type ClaimsShort struct {
jwt.RegisteredClaims jwt.RegisteredClaims
} }
type OIDCAddress struct {
Formatted string `json:"formatted"`
StreetAddress string `json:"street_address"`
Locality string `json:"locality"`
Region string `json:"region"`
PostalCode string `json:"postal_code"`
Country string `json:"country"`
}
type ClaimsWithoutThirdIdp struct { type ClaimsWithoutThirdIdp struct {
*UserWithoutThirdIdp *UserWithoutThirdIdp
TokenType string `json:"tokenType,omitempty"` TokenType string `json:"tokenType,omitempty"`
@ -245,6 +255,8 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
LastSigninWrongTime: user.LastSigninWrongTime, LastSigninWrongTime: user.LastSigninWrongTime,
SigninWrongTimes: user.SigninWrongTimes, SigninWrongTimes: user.SigninWrongTimes,
ManagedAccounts: user.ManagedAccounts,
} }
return res return res
@ -356,6 +368,10 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
}, },
} }
if application.IsShared {
claims.Audience = []string{application.ClientId + "-org-" + user.Owner}
}
var token *jwt.Token var token *jwt.Token
var refreshToken *jwt.Token var refreshToken *jwt.Token
@ -363,29 +379,52 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
application.TokenFormat = "JWT" application.TokenFormat = "JWT"
} }
var jwtMethod jwt.SigningMethod
if application.TokenSigningMethod == "RS256" {
jwtMethod = jwt.SigningMethodRS256
} else if application.TokenSigningMethod == "RS512" {
jwtMethod = jwt.SigningMethodRS512
} else if application.TokenSigningMethod == "ES256" {
jwtMethod = jwt.SigningMethodES256
} else if application.TokenSigningMethod == "ES512" {
jwtMethod = jwt.SigningMethodES512
} else if application.TokenSigningMethod == "ES384" {
jwtMethod = jwt.SigningMethodES384
} else {
jwtMethod = jwt.SigningMethodRS256
}
// the JWT token length in "JWT-Empty" mode will be very short, as User object only has two properties: owner and name // the JWT token length in "JWT-Empty" mode will be very short, as User object only has two properties: owner and name
if application.TokenFormat == "JWT" { if application.TokenFormat == "JWT" {
claimsWithoutThirdIdp := getClaimsWithoutThirdIdp(claims) claimsWithoutThirdIdp := getClaimsWithoutThirdIdp(claims)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp) token = jwt.NewWithClaims(jwtMethod, claimsWithoutThirdIdp)
claimsWithoutThirdIdp.ExpiresAt = jwt.NewNumericDate(refreshExpireTime) claimsWithoutThirdIdp.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claimsWithoutThirdIdp.TokenType = "refresh-token" claimsWithoutThirdIdp.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp) refreshToken = jwt.NewWithClaims(jwtMethod, claimsWithoutThirdIdp)
} else if application.TokenFormat == "JWT-Empty" { } else if application.TokenFormat == "JWT-Empty" {
claimsShort := getShortClaims(claims) claimsShort := getShortClaims(claims)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort) token = jwt.NewWithClaims(jwtMethod, claimsShort)
claimsShort.ExpiresAt = jwt.NewNumericDate(refreshExpireTime) claimsShort.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claimsShort.TokenType = "refresh-token" claimsShort.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort) refreshToken = jwt.NewWithClaims(jwtMethod, claimsShort)
} else if application.TokenFormat == "JWT-Custom" { } else if application.TokenFormat == "JWT-Custom" {
claimsCustom := getClaimsCustom(claims, application.TokenFields) claimsCustom := getClaimsCustom(claims, application.TokenFields)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsCustom) token = jwt.NewWithClaims(jwtMethod, claimsCustom)
refreshClaims := getClaimsCustom(claims, application.TokenFields) refreshClaims := getClaimsCustom(claims, application.TokenFields)
refreshClaims["exp"] = jwt.NewNumericDate(refreshExpireTime) refreshClaims["exp"] = jwt.NewNumericDate(refreshExpireTime)
refreshClaims["TokenType"] = "refresh-token" refreshClaims["TokenType"] = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, refreshClaims) refreshToken = jwt.NewWithClaims(jwtMethod, refreshClaims)
} else if application.TokenFormat == "JWT-Standard" {
claimsStandard := getStandardClaims(claims)
token = jwt.NewWithClaims(jwtMethod, claimsStandard)
claimsStandard.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claimsStandard.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwtMethod, claimsStandard)
} else { } else {
return "", "", "", fmt.Errorf("unknown application TokenFormat: %s", application.TokenFormat) return "", "", "", fmt.Errorf("unknown application TokenFormat: %s", application.TokenFormat)
} }
@ -403,34 +442,57 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
} }
} }
// RSA private key var (
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey)) tokenString string
refreshTokenString string
key interface{}
)
if strings.Contains(application.TokenSigningMethod, "RS") || application.TokenSigningMethod == "" {
// RSA private key
key, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey))
} else if strings.Contains(application.TokenSigningMethod, "ES") {
// ES private key
key, err = jwt.ParseECPrivateKeyFromPEM([]byte(cert.PrivateKey))
} else if strings.Contains(application.TokenSigningMethod, "Ed") {
// Ed private key
key, err = jwt.ParseEdPrivateKeyFromPEM([]byte(cert.PrivateKey))
}
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
token.Header["kid"] = cert.Name token.Header["kid"] = cert.Name
tokenString, err := token.SignedString(key) tokenString, err = token.SignedString(key)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
refreshTokenString, err := refreshToken.SignedString(key) refreshTokenString, err = refreshToken.SignedString(key)
return tokenString, refreshTokenString, name, err return tokenString, refreshTokenString, name, err
} }
func ParseJwtToken(token string, cert *Cert) (*Claims, error) { func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { var (
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) certificate interface{}
} err error
)
if cert.Certificate == "" { if cert.Certificate == "" {
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert) return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
} }
// RSA certificate if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate)) // RSA certificate
certificate, err = jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
} else if _, ok := token.Method.(*jwt.SigningMethodECDSA); ok {
// ES certificate
certificate, err = jwt.ParseECPublicKeyFromPEM([]byte(cert.Certificate))
} else {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -309,12 +309,22 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
}, nil }, nil
} }
_, err = ParseJwtToken(refreshToken, cert) if application.TokenFormat == "JWT-Standard" {
if err != nil { _, err = ParseStandardJwtToken(refreshToken, cert)
return &TokenError{ if err != nil {
Error: InvalidGrant, return &TokenError{
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()), Error: InvalidGrant,
}, nil ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil
}
} else {
_, err = ParseJwtToken(refreshToken, cert)
if err != nil {
return &TokenError{
Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil
}
} }
// generate a new token // generate a new token
@ -418,22 +428,26 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
if token == nil { if token == nil {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "authorization code is invalid", ErrorDescription: fmt.Sprintf("authorization code: [%s] is invalid", code),
}, nil }, nil
} }
if token.CodeIsUsed { if token.CodeIsUsed {
// anti replay attacks // anti replay attacks
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "authorization code has been used", ErrorDescription: fmt.Sprintf("authorization code has been used for token: [%s]", token.GetId()),
}, nil }, nil
} }
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge { if token.CodeChallenge != "" {
return nil, &TokenError{ challengeAnswer := pkceChallenge(verifier)
Error: InvalidGrant, if challengeAnswer != token.CodeChallenge {
ErrorDescription: "verifier is invalid", return nil, &TokenError{
}, nil Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("verifier is invalid, challengeAnswer: [%s], token.CodeChallenge: [%s]", challengeAnswer, token.CodeChallenge),
}, nil
}
} }
if application.ClientSecret != clientSecret { if application.ClientSecret != clientSecret {
@ -442,13 +456,13 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
if token.CodeChallenge == "" { if token.CodeChallenge == "" {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidClient, Error: InvalidClient,
ErrorDescription: "client_secret is invalid", ErrorDescription: fmt.Sprintf("client_secret is invalid for application: [%s], token.CodeChallenge: empty", application.GetId()),
}, nil }, nil
} else { } else {
if clientSecret != "" { if clientSecret != "" {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidClient, Error: InvalidClient,
ErrorDescription: "client_secret is invalid", ErrorDescription: fmt.Sprintf("client_secret is invalid for application: [%s], token.CodeChallenge: [%s]", application.GetId(), token.CodeChallenge),
}, nil }, nil
} }
} }
@ -457,15 +471,16 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
if application.Name != token.Application { if application.Name != token.Application {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "the token is for wrong application (client_id)", ErrorDescription: fmt.Sprintf("the token is for wrong application (client_id), application.Name: [%s], token.Application: [%s]", application.Name, token.Application),
}, nil }, nil
} }
if time.Now().Unix() > token.CodeExpireIn { nowUnix := time.Now().Unix()
if nowUnix > token.CodeExpireIn {
// code must be used within 5 minutes // code must be used within 5 minutes
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "authorization code has expired", ErrorDescription: fmt.Sprintf("authorization code has expired, nowUnix: [%s], token.CodeExpireIn: [%s]", time.Unix(nowUnix, 0).Format(time.RFC3339), time.Unix(token.CodeExpireIn, 0).Format(time.RFC3339)),
}, nil }, nil
} }
return token, nil, nil return token, nil, nil

View File

@ -0,0 +1,121 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"github.com/golang-jwt/jwt/v4"
)
type ClaimsStandard struct {
*UserShort
EmailVerified bool `json:"email_verified,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
Gender string `json:"gender,omitempty"`
TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
Address OIDCAddress `json:"address,omitempty"`
jwt.RegisteredClaims
}
func getStreetAddress(user *User) string {
var addrs string
for _, addr := range user.Address {
addrs += addr + "\n"
}
return addrs
}
func getStandardClaims(claims Claims) ClaimsStandard {
res := ClaimsStandard{
UserShort: getShortUser(claims.User),
EmailVerified: claims.User.EmailVerified,
TokenType: claims.TokenType,
Nonce: claims.Nonce,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
}
res.Phone = ""
var scopes []string
if strings.Contains(claims.Scope, ",") {
scopes = strings.Split(claims.Scope, ",")
} else {
scopes = strings.Split(claims.Scope, " ")
}
for _, scope := range scopes {
if scope == "address" {
res.Address = OIDCAddress{StreetAddress: getStreetAddress(claims.User)}
} else if scope == "profile" {
res.Gender = claims.User.Gender
} else if scope == "phone" && claims.User.Phone != "" {
res.PhoneNumberVerified = true
phoneNumber, ok := util.GetE164Number(claims.User.Phone, claims.User.CountryCode)
if !ok {
res.PhoneNumberVerified = false
} else {
res.PhoneNumber = phoneNumber
}
}
}
return res
}
func ParseStandardJwtToken(token string, cert *Cert) (*ClaimsStandard, error) {
t, err := jwt.ParseWithClaims(token, &ClaimsStandard{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
if cert.Certificate == "" {
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
// RSA certificate
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
if err != nil {
return nil, err
}
return certificate, nil
})
if t != nil {
if claims, ok := t.Claims.(*ClaimsStandard); ok && t.Valid {
return claims, nil
}
}
return nil, err
}
func ParseStandardJwtTokenByApplication(token string, application *Application) (*ClaimsStandard, error) {
cert, err := getCertByApplication(application)
if err != nil {
return nil, err
}
return ParseStandardJwtToken(token, cert)
}

View File

@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
@ -43,7 +44,7 @@ type Transaction struct {
Application string `xorm:"varchar(100)" json:"application"` Application string `xorm:"varchar(100)" json:"application"`
Payment string `xorm:"varchar(100)" json:"payment"` Payment string `xorm:"varchar(100)" json:"payment"`
State string `xorm:"varchar(100)" json:"state"` State pp.PaymentState `xorm:"varchar(100)" json:"state"`
} }
func GetTransactionCount(owner, field, value string) (int64, error) { func GetTransactionCount(owner, field, value string) (int64, error) {

View File

@ -203,7 +203,9 @@ type User struct {
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"` SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
} }
type Userinfo struct { type Userinfo struct {
@ -229,6 +231,12 @@ type ManagedAccount struct {
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"` SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
} }
type MfaAccount struct {
AccountName string `xorm:"varchar(100)" json:"accountName"`
Issuer string `xorm:"varchar(100)" json:"issuer"`
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
}
type FaceId struct { type FaceId struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
FaceIdData []float64 `json:"faceIdData"` FaceIdData []float64 `json:"faceIdData"`
@ -602,6 +610,12 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
} }
} }
if user.MfaAccounts != nil {
for _, mfaAccount := range user.MfaAccounts {
mfaAccount.SecretKey = "***"
}
}
if user.TotpSecret != "" { if user.TotpSecret != "" {
user.TotpSecret = "" user.TotpSecret = ""
} }
@ -674,7 +688,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
columns = []string{ columns = []string{
"owner", "display_name", "avatar", "first_name", "last_name", "owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon", "baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
@ -682,11 +696,11 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup", "eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo", "spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "yammer", "yandex", "zoom", "custom", "need_update_password",
} }
} }
if isAdmin { if isAdmin {
columns = append(columns, "name", "id", "email", "phone", "country_code", "type") columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance")
} }
columns = append(columns, "updated_time") columns = append(columns, "updated_time")
@ -1124,7 +1138,7 @@ func (user *User) IsApplicationAdmin(application *Application) bool {
return false return false
} }
return (user.Owner == application.Organization && user.IsAdmin) || user.IsGlobalAdmin() return (user.Owner == application.Organization && user.IsAdmin) || user.IsGlobalAdmin() || (user.IsAdmin && application.IsShared)
} }
func (user *User) IsGlobalAdmin() bool { func (user *User) IsGlobalAdmin() bool {
@ -1156,3 +1170,13 @@ func GenerateIdForNewUser(application *Application) (string, error) {
res := strconv.Itoa(lastUserId + 1) res := strconv.Itoa(lastUserId + 1)
return res, nil return res, nil
} }
func UpdateUserBalance(owner string, name string, balance float64) error {
user, err := getUser(owner, name)
if err != nil {
return err
}
user.Balance += balance
_, err = UpdateUser(user.GetId(), user, []string{"balance"}, true)
return err
}

View File

@ -393,6 +393,20 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
} }
if oldUser.Address == nil {
oldUser.Address = []string{}
}
oldUserAddressJson, _ := json.Marshal(oldUser.Address)
if newUser.Address == nil {
newUser.Address = []string{}
}
newUserAddressJson, _ := json.Marshal(newUser.Address)
if string(oldUserAddressJson) != string(newUserAddressJson) {
item := GetAccountItemByName("Address", organization)
itemsChanged = append(itemsChanged, item)
}
if newUser.FaceIds != nil { if newUser.FaceIds != nil {
item := GetAccountItemByName("Face ID", organization) item := GetAccountItemByName("Face ID", organization)
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
@ -411,12 +425,46 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
item := GetAccountItemByName("Is deleted", organization) item := GetAccountItemByName("Is deleted", organization)
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
} }
if oldUser.NeedUpdatePassword != newUser.NeedUpdatePassword {
item := GetAccountItemByName("Need update password", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Balance != newUser.Balance {
item := GetAccountItemByName("Balance", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Score != newUser.Score { if oldUser.Score != newUser.Score {
item := GetAccountItemByName("Score", organization) item := GetAccountItemByName("Score", organization)
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
} }
if oldUser.Karma != newUser.Karma {
item := GetAccountItemByName("Karma", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Language != newUser.Language {
item := GetAccountItemByName("Language", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Ranking != newUser.Ranking {
item := GetAccountItemByName("Ranking", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Currency != newUser.Currency {
item := GetAccountItemByName("Currency", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Hash != newUser.Hash {
item := GetAccountItemByName("Hash", organization)
itemsChanged = append(itemsChanged, item)
}
for _, accountItem := range itemsChanged { for _, accountItem := range itemsChanged {
if pass, err := CheckAccountItemModifyRule(accountItem, isAdmin, lang); !pass { if pass, err := CheckAccountItemModifyRule(accountItem, isAdmin, lang); !pass {

50
pp/balance.go Normal file
View File

@ -0,0 +1,50 @@
// Copyright 2024 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 pp
import (
"fmt"
"github.com/casdoor/casdoor/util"
)
type BalancePaymentProvider struct{}
func NewBalancePaymentProvider() (*BalancePaymentProvider, error) {
pp := &BalancePaymentProvider{}
return pp, nil
}
func (pp *BalancePaymentProvider) Pay(r *PayReq) (*PayResp, error) {
owner, _ := util.GetOwnerAndNameFromId(r.PayerId)
return &PayResp{
PayUrl: r.ReturnUrl,
OrderId: fmt.Sprintf("%s/%s", owner, r.PaymentName),
}, nil
}
func (pp *BalancePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
return &NotifyResult{
PaymentStatus: PaymentStatePaid,
}, nil
}
func (pp *BalancePaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
return "", nil
}
func (pp *BalancePaymentProvider) GetResponseError(err error) string {
return ""
}

View File

@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"log" "log"
"strings" "strings"
"time"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -27,6 +28,14 @@ import (
"layeh.com/radius/rfc2866" "layeh.com/radius/rfc2866"
) )
var StateMap map[string]AccessStateContent
const StateExpiredTime = time.Second * 120
type AccessStateContent struct {
ExpiredAt time.Time
}
func StartRadiusServer() { func StartRadiusServer() {
secret := conf.GetConfigString("radiusSecret") secret := conf.GetConfigString("radiusSecret")
server := radius.PacketServer{ server := radius.PacketServer{
@ -55,6 +64,7 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
username := rfc2865.UserName_GetString(r.Packet) username := rfc2865.UserName_GetString(r.Packet)
password := rfc2865.UserPassword_GetString(r.Packet) password := rfc2865.UserPassword_GetString(r.Packet)
organization := rfc2865.Class_GetString(r.Packet) organization := rfc2865.Class_GetString(r.Packet)
state := rfc2865.State_GetString(r.Packet)
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password) log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
if organization == "" { if organization == "" {
@ -62,12 +72,75 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
return return
} }
_, err := object.CheckUserPassword(organization, username, password, "en") var user *object.User
var err error
if state == "" {
user, err = object.CheckUserPassword(organization, username, password, "en")
} else {
user, err = object.GetUser(fmt.Sprintf("%s/%s", organization, username))
}
if err != nil { if err != nil {
w.Write(r.Response(radius.CodeAccessReject)) w.Write(r.Response(radius.CodeAccessReject))
return return
} }
if user.IsMfaEnabled() {
mfaProp := user.GetMfaProps(object.TotpType, false)
if mfaProp == nil {
w.Write(r.Response(radius.CodeAccessReject))
return
}
if StateMap == nil {
StateMap = map[string]AccessStateContent{}
}
if state != "" {
stateContent, ok := StateMap[state]
if !ok {
w.Write(r.Response(radius.CodeAccessReject))
return
}
delete(StateMap, state)
if stateContent.ExpiredAt.Before(time.Now()) {
w.Write(r.Response(radius.CodeAccessReject))
return
}
mfaUtil := object.GetMfaUtil(mfaProp.MfaType, mfaProp)
if mfaUtil.Verify(password) != nil {
w.Write(r.Response(radius.CodeAccessReject))
return
}
w.Write(r.Response(radius.CodeAccessAccept))
return
}
responseState := util.GenerateId()
StateMap[responseState] = AccessStateContent{
time.Now().Add(StateExpiredTime),
}
err = rfc2865.State_Set(r.Packet, []byte(responseState))
if err != nil {
w.Write(r.Response(radius.CodeAccessReject))
return
}
err = rfc2865.ReplyMessage_Set(r.Packet, []byte("please enter OTP"))
if err != nil {
w.Write(r.Response(radius.CodeAccessReject))
return
}
r.Packet.Code = radius.CodeAccessChallenge
w.Write(r.Packet)
}
w.Write(r.Response(radius.CodeAccessAccept)) w.Write(r.Response(radius.CodeAccessAccept))
} }

View File

@ -35,20 +35,13 @@ type Object struct {
} }
func getUsername(ctx *context.Context) (username string) { func getUsername(ctx *context.Context) (username string) {
defer func() { username, ok := ctx.Input.Session("username").(string)
if r := recover(); r != nil { if !ok || username == "" {
username, _ = getUsernameByClientIdSecret(ctx)
}
}()
username = ctx.Input.Session("username").(string)
if username == "" {
username, _ = getUsernameByClientIdSecret(ctx) username, _ = getUsernameByClientIdSecret(ctx)
} }
if username == "" { if username == "" {
username = getUsernameByKeys(ctx) username, _ = getUsernameByKeys(ctx)
} }
return return
} }
@ -63,7 +56,7 @@ func getSubject(ctx *context.Context) (string, string) {
return util.GetOwnerAndNameFromId(username) return util.GetOwnerAndNameFromId(username)
} }
func getObject(ctx *context.Context) (string, string) { func getObject(ctx *context.Context) (string, string, error) {
method := ctx.Request.Method method := ctx.Request.Method
path := ctx.Request.URL.Path path := ctx.Request.URL.Path
@ -72,13 +65,13 @@ func getObject(ctx *context.Context) (string, string) {
if ctx.Input.Query("id") == "/" { if ctx.Input.Query("id") == "/" {
adapterId := ctx.Input.Query("adapterId") adapterId := ctx.Input.Query("adapterId")
if adapterId != "" { if adapterId != "" {
return util.GetOwnerAndNameFromIdNoCheck(adapterId) return util.GetOwnerAndNameFromIdWithError(adapterId)
} }
} else { } else {
// query == "?id=built-in/admin" // query == "?id=built-in/admin"
id := ctx.Input.Query("id") id := ctx.Input.Query("id")
if id != "" { if id != "" {
return util.GetOwnerAndNameFromIdNoCheck(id) return util.GetOwnerAndNameFromIdWithError(id)
} }
} }
} }
@ -87,34 +80,34 @@ func getObject(ctx *context.Context) (string, string) {
// query == "?id=built-in/admin" // query == "?id=built-in/admin"
id := ctx.Input.Query("id") id := ctx.Input.Query("id")
if id != "" { if id != "" {
return util.GetOwnerAndNameFromIdNoCheck(id) return util.GetOwnerAndNameFromIdWithError(id)
} }
} }
owner := ctx.Input.Query("owner") owner := ctx.Input.Query("owner")
if owner != "" { if owner != "" {
return owner, "" return owner, "", nil
} }
return "", "" return "", "", nil
} else { } else {
if path == "/api/add-policy" || path == "/api/remove-policy" || path == "/api/update-policy" { if path == "/api/add-policy" || path == "/api/remove-policy" || path == "/api/update-policy" {
id := ctx.Input.Query("id") id := ctx.Input.Query("id")
if id != "" { if id != "" {
return util.GetOwnerAndNameFromIdNoCheck(id) return util.GetOwnerAndNameFromIdWithError(id)
} }
} }
body := ctx.Input.RequestBody body := ctx.Input.RequestBody
if len(body) == 0 { if len(body) == 0 {
return ctx.Request.Form.Get("owner"), ctx.Request.Form.Get("name") return ctx.Request.Form.Get("owner"), ctx.Request.Form.Get("name"), nil
} }
var obj Object var obj Object
err := json.Unmarshal(body, &obj) err := json.Unmarshal(body, &obj)
if err != nil { if err != nil {
// panic(err) // this is not error
return "", "" return "", "", nil
} }
if path == "/api/delete-resource" { if path == "/api/delete-resource" {
@ -124,7 +117,7 @@ func getObject(ctx *context.Context) (string, string) {
} }
} }
return obj.Owner, obj.Name return obj.Owner, obj.Name, nil
} }
} }
@ -190,7 +183,12 @@ func ApiFilter(ctx *context.Context) {
objOwner, objName := "", "" objOwner, objName := "", ""
if urlPath != "/api/get-app-login" && urlPath != "/api/get-resource" { if urlPath != "/api/get-app-login" && urlPath != "/api/get-resource" {
objOwner, objName = getObject(ctx) var err error
objOwner, objName, err = getObject(ctx)
if err != nil {
responseError(ctx, err.Error())
return
}
} }
if strings.HasPrefix(urlPath, "/api/notify-payment") { if strings.HasPrefix(urlPath, "/api/notify-payment") {

View File

@ -16,6 +16,7 @@ package routers
import ( import (
"fmt" "fmt"
"strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -23,6 +24,10 @@ import (
) )
func AutoSigninFilter(ctx *context.Context) { func AutoSigninFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
if strings.HasPrefix(urlPath, "/api/login/oauth/access_token") {
return
}
//if getSessionUser(ctx) != "" { //if getSessionUser(ctx) != "" {
// return // return
//} //}
@ -67,6 +72,17 @@ func AutoSigninFilter(ctx *context.Context) {
return return
} }
accessKey := ctx.Input.Query("accessKey")
accessSecret := ctx.Input.Query("accessSecret")
if accessKey != "" && accessSecret != "" {
userId, err := getUsernameByKeys(ctx)
if err != nil {
responseError(ctx, err.Error())
}
setSessionUser(ctx, userId)
}
// "/page?clientId=123&clientSecret=456" // "/page?clientId=123&clientSecret=456"
userId, err := getUsernameByClientIdSecret(ctx) userId, err := getUsernameByClientIdSecret(ctx)
if err != nil { if err != nil {

View File

@ -91,17 +91,22 @@ func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
return fmt.Sprintf("app/%s", application.Name), nil return fmt.Sprintf("app/%s", application.Name), nil
} }
func getUsernameByKeys(ctx *context.Context) string { func getUsernameByKeys(ctx *context.Context) (string, error) {
accessKey, accessSecret := getKeys(ctx) accessKey, accessSecret := getKeys(ctx)
user, err := object.GetUserByAccessKey(accessKey) user, err := object.GetUserByAccessKey(accessKey)
if err != nil { if err != nil {
panic(err) return "", err
} }
if user != nil && accessSecret == user.AccessSecret { if user == nil {
return user.GetId() return "", fmt.Errorf("user not found for access key: %s", accessKey)
} }
return ""
if accessSecret != user.AccessSecret {
return "", fmt.Errorf("incorrect access secret for user: %s", user.Name)
}
return user.GetId(), nil
} }
func getSessionUser(ctx *context.Context) string { func getSessionUser(ctx *context.Context) string {

View File

@ -58,7 +58,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
redirectUri := ctx.Input.Query("redirect_uri") redirectUri := ctx.Input.Query("redirect_uri")
scope := ctx.Input.Query("scope") scope := ctx.Input.Query("scope")
state := ctx.Input.Query("state") state := ctx.Input.Query("state")
nonce := "" nonce := ctx.Input.Query("nonce")
codeChallenge := ctx.Input.Query("code_challenge") codeChallenge := ctx.Input.Query("code_challenge")
if clientId == "" || responseType != "code" || redirectUri == "" { if clientId == "" || responseType != "code" || redirectUri == "" {
return "", nil return "", nil

19
storage/casdoor.go Normal file
View File

@ -0,0 +1,19 @@
package storage
import (
"github.com/casdoor/oss"
"github.com/casdoor/oss/casdoor"
)
func NewCasdoorStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string, cert string, content string) oss.StorageInterface {
sp := casdoor.New(&casdoor.Config{
clientId,
clientSecret,
endpoint,
cert,
region,
content,
bucket,
})
return sp
}

View File

@ -16,7 +16,7 @@ package storage
import "github.com/casdoor/oss" import "github.com/casdoor/oss"
func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string) (oss.StorageInterface, error) { func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string, cert string, content string) (oss.StorageInterface, error) {
switch providerType { switch providerType {
case "Local File System": case "Local File System":
return NewLocalFileSystemStorageProvider(), nil return NewLocalFileSystemStorageProvider(), nil
@ -36,6 +36,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
return NewGoogleCloudStorageProvider(clientSecret, bucket, endpoint), nil return NewGoogleCloudStorageProvider(clientSecret, bucket, endpoint), nil
case "Synology": case "Synology":
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil
case "Casdoor":
return NewCasdoorStorageProvider(providerType, clientId, clientSecret, region, bucket, endpoint, cert, content), nil
} }
return nil, nil return nil, nil

View File

@ -131,6 +131,15 @@ func GetOwnerAndNameFromId(id string) (string, string) {
return tokens[0], tokens[1] return tokens[0], tokens[1]
} }
func GetOwnerAndNameFromIdWithError(id string) (string, string, error) {
tokens := strings.Split(id, "/")
if len(tokens) != 2 {
return "", "", errors.New("GetOwnerAndNameFromId() error, wrong token count for ID: " + id)
}
return tokens[0], tokens[1], nil
}
func GetOwnerFromId(id string) string { func GetOwnerFromId(id string) string {
tokens := strings.Split(id, "/") tokens := strings.Split(id, "/")
if len(tokens) != 2 { if len(tokens) != 2 {
@ -154,6 +163,16 @@ func GetOwnerAndNameAndOtherFromId(id string) (string, string, string) {
return tokens[0], tokens[1], tokens[2] return tokens[0], tokens[1], tokens[2]
} }
func GetSharedOrgFromApp(rawName string) (name string, organization string) {
name = rawName
splitName := strings.Split(rawName, "-org-")
if len(splitName) >= 2 {
organization = splitName[len(splitName)-1]
name = splitName[0]
}
return name, organization
}
func GenerateId() string { func GenerateId() string {
return uuid.NewString() return uuid.NewString()
} }
@ -354,9 +373,16 @@ func StringToInterfaceArray(array []string) []interface{} {
func StringToInterfaceArray2d(arrays [][]string) [][]interface{} { func StringToInterfaceArray2d(arrays [][]string) [][]interface{} {
var interfaceArrays [][]interface{} var interfaceArrays [][]interface{}
for _, req := range arrays { for _, req := range arrays {
var interfaceArray []interface{} var (
for _, r := range req { interfaceArray []interface{}
interfaceArray = append(interfaceArray, r) elem interface{}
)
for _, elem = range req {
jStruct, err := TryJsonToAnonymousStruct(elem.(string))
if err == nil {
elem = jStruct
}
interfaceArray = append(interfaceArray, elem)
} }
interfaceArrays = append(interfaceArrays, interfaceArray) interfaceArrays = append(interfaceArrays, interfaceArray)
} }

View File

@ -252,8 +252,8 @@ class AdapterEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:DB test"), i18next.t("provider:DB test - Tooltip"))} : {Setting.getLabel(i18next.t("provider:DB test"), i18next.t("provider:DB test - Tooltip"))} :
</Col> </Col>
<Col span={2} > <Col span={2} >
<Button type={"primary"} onClick={() => { <Button disabled={this.state.organizationName !== this.state.adapter.owner} type={"primary"} onClick={() => {
AdapterBackend.getPolicies("", "", `${this.state.organizationName}/${this.state.adapterName}`) AdapterBackend.getPolicies("", "", `${this.state.adapter.owner}/${this.state.adapter.name}`)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("syncer:Connect successfully")); Setting.showMessage("success", i18next.t("syncer:Connect successfully"));
@ -279,13 +279,14 @@ class AdapterEditPage extends React.Component {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({ this.setState({
organizationName: this.state.adapter.owner,
adapterName: this.state.adapter.name, adapterName: this.state.adapter.name,
}); });
if (exitAfterSave) { if (exitAfterSave) {
this.props.history.push("/adapters"); this.props.history.push("/adapters");
} else { } else {
this.props.history.push(`/adapters/${this.state.organizationName}/${this.state.adapter.name}`); this.props.history.push(`/adapters/${this.state.adapter.owner}/${this.state.adapter.name}`);
} }
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);

View File

@ -56,9 +56,11 @@ class AdapterListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -16,6 +16,7 @@ import React, {Component, Suspense, lazy} 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 {setOrgIsTourVisible, setTourLogo} from "./TourConfig";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {GithubOutlined, InfoCircleFilled, ShareAltOutlined} from "@ant-design/icons"; import {GithubOutlined, InfoCircleFilled, ShareAltOutlined} from "@ant-design/icons";
import {Alert, Button, ConfigProvider, Drawer, FloatButton, Layout, Result, Tooltip} from "antd"; import {Alert, Button, ConfigProvider, Drawer, FloatButton, Layout, Result, Tooltip} from "antd";
@ -247,6 +248,8 @@ class App extends Component {
this.setLanguage(account); this.setLanguage(account);
this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm); this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm);
setTourLogo(account.organization.logo);
setOrgIsTourVisible(account.organization.enableTour);
} else { } else {
if (res.data !== "Please login first") { if (res.data !== "Please login first") {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
@ -341,7 +344,8 @@ class App extends Component {
window.location.pathname.startsWith("/cas") || window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/select-plan") || window.location.pathname.startsWith("/select-plan") ||
window.location.pathname.startsWith("/buy-plan") || window.location.pathname.startsWith("/buy-plan") ||
window.location.pathname.startsWith("/qrcode") ; window.location.pathname.startsWith("/qrcode") ||
window.location.pathname.startsWith("/captcha");
} }
onClick = ({key}) => { onClick = ({key}) => {
@ -414,6 +418,7 @@ class App extends Component {
<Layout id="parent-area"> <Layout id="parent-area">
<ManagementPage <ManagementPage
account={this.state.account} account={this.state.account}
application={this.state.application}
uri={this.state.uri} uri={this.state.uri}
themeData={this.state.themeData} themeData={this.state.themeData}
themeAlgorithm={this.state.themeAlgorithm} themeAlgorithm={this.state.themeAlgorithm}

View File

@ -116,7 +116,6 @@ class ApplicationEditPage extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getApplication(); this.getApplication();
this.getOrganizations(); this.getOrganizations();
this.getProviders();
} }
getApplication() { getApplication() {
@ -145,7 +144,9 @@ class ApplicationEditPage extends React.Component {
application: application, application: application,
}); });
this.getCerts(application.organization); this.getProviders(application);
this.getCerts(application);
this.getSamlMetadata(application.enableSamlPostBinding); this.getSamlMetadata(application.enableSamlPostBinding);
}); });
@ -166,7 +167,11 @@ class ApplicationEditPage extends React.Component {
}); });
} }
getCerts(owner) { getCerts(application) {
let owner = application.organization;
if (application.isShared) {
owner = this.props.owner;
}
CertBackend.getCerts(owner) CertBackend.getCerts(owner)
.then((res) => { .then((res) => {
this.setState({ this.setState({
@ -175,8 +180,12 @@ class ApplicationEditPage extends React.Component {
}); });
} }
getProviders() { getProviders(application) {
ProviderBackend.getProviders(this.state.owner) let owner = application.organization;
if (application.isShared) {
owner = this.props.account.owner;
}
ProviderBackend.getProviders(owner)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.setState({ this.setState({
@ -263,6 +272,16 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Is shared"), i18next.t("general:Is shared - Tooltip"))} :
</Col>
<Col span={22} >
<Switch disabled={Setting.isAdminUser()} checked={this.state.application.isShared} onChange={checked => {
this.updateApplicationField("isShared", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} : {Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} :
@ -384,7 +403,17 @@ class ApplicationEditPage extends React.Component {
</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);})}
options={["JWT", "JWT-Empty", "JWT-Custom"].map((item) => Setting.getOption(item, item))} options={["JWT", "JWT-Empty", "JWT-Custom", "JWT-Standard"].map((item) => Setting.getOption(item, item))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Token signing method"), i18next.t("application:Token signing method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenSigningMethod === "" ? "RS256" : this.state.application.tokenSigningMethod} onChange={(value => {this.updateApplicationField("tokenSigningMethod", value);})}
options={["RS256", "RS512", "ES256", "ES512", "ES384"].map((item) => Setting.getOption(item, item))}
/> />
</Col> </Col>
</Row> </Row>
@ -989,7 +1018,11 @@ class ApplicationEditPage extends React.Component {
redirectUri = "\"ERROR: You must specify at least one Redirect URL in 'Redirect URLs'\""; redirectUri = "\"ERROR: You must specify at least one Redirect URL in 'Redirect URLs'\"";
} }
const signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=read&state=casdoor`; let clientId = this.state.application.clientId;
if (this.state.application.isShared) {
clientId += `-org-${this.props.account.owner}`;
}
const signInUrl = `/login/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=read&state=casdoor`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"}; const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"};
if (!Setting.isPasswordEnabled(this.state.application)) { if (!Setting.isPasswordEnabled(this.state.application)) {
signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize"); signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize");

View File

@ -97,9 +97,11 @@ class ApplicationListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
@ -123,7 +125,7 @@ class ApplicationListPage extends BaseListPage {
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/applications/${record.organization}/${text}`}> <Link to={`/applications/${record.organization}/${text}`}>
{text} {Setting.getApplicationDisplayName(record)}
</Link> </Link>
); );
}, },

116
web/src/CaptchaPage.js Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {CaptchaModal} from "./common/modal/CaptchaModal";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Setting from "./Setting";
class CaptchaPage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(this.props.location.search);
this.state = {
owner: "admin",
application: null,
clientId: params.get("client_id"),
applicationName: params.get("state"),
redirectUri: params.get("redirect_uri"),
};
}
componentDidMount() {
this.getApplication();
}
onUpdateApplication(application) {
this.setState({
application: application,
});
}
getApplication() {
if (this.state.applicationName === null) {
return null;
}
ApplicationBackend.getApplication(this.state.owner, this.state.applicationName)
.then((res) => {
if (res.status === "error") {
this.onUpdateApplication(null);
this.setState({
msg: res.msg,
});
return ;
}
this.onUpdateApplication(res.data);
});
}
getCaptchaProviderItems(application) {
const providers = application?.providers;
if (providers === undefined || providers === null) {
return null;
}
return providers.filter(providerItem => {
if (providerItem.provider === undefined || providerItem.provider === null) {
return false;
}
return providerItem.provider.category === "Captcha";
});
}
callback(values) {
Setting.goToLink(`${this.state.redirectUri}?code=${values.captchaToken}&type=${values.captchaType}&secret=${values.clientSecret}&applicationId=${values.applicationId}`);
}
renderCaptchaModal(application) {
const captchaProviderItems = this.getCaptchaProviderItems(application);
if (captchaProviderItems === null) {
return null;
}
const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always");
const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic");
const provider = alwaysProviderItems.length > 0
? alwaysProviderItems[0].provider
: dynamicProviderItems[0].provider;
return <CaptchaModal
owner={provider.owner}
name={provider.name}
visible={true}
onOk={(captchaType, captchaToken, clientSecret) => {
const values = {
captchaType: captchaType,
captchaToken: captchaToken,
clientSecret: clientSecret,
applicationId: `${provider.owner}/${provider.name}`,
};
this.callback(values);
}}
onCancel={() => this.callback({captchaType: "none", captchaToken: "", clientSecret: ""})}
isCurrentProvider={true}
/>;
}
render() {
return (
this.renderCaptchaModal(this.state.application)
);
}
}
export default CaptchaPage;

View File

@ -288,14 +288,14 @@ class CertEditPage extends React.Component {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({ this.setState({
certName: this.state.cert.name, certName: this.state.cert.name,
}, () => {
if (exitAfterSave) {
this.props.history.push("/certs");
} else {
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`);
this.getCert();
}
}); });
if (exitAfterSave) {
this.props.history.push("/certs");
} else {
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`);
this.getCert();
}
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateCertField("name", this.state.certName); this.updateCertField("name", this.state.certName);

View File

@ -73,9 +73,11 @@ class CertListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -55,9 +55,11 @@ class EnforcerListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -32,6 +32,7 @@ import {authConfig} from "./auth/Auth";
import ProductBuyPage from "./ProductBuyPage"; import ProductBuyPage from "./ProductBuyPage";
import PaymentResultPage from "./PaymentResultPage"; import PaymentResultPage from "./PaymentResultPage";
import QrCodePage from "./QrCodePage"; import QrCodePage from "./QrCodePage";
import CaptchaPage from "./CaptchaPage";
import CustomHead from "./basic/CustomHead"; import CustomHead from "./basic/CustomHead";
class EntryPage extends React.Component { class EntryPage extends React.Component {
@ -108,8 +109,8 @@ class EntryPage extends React.Component {
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/forget" render={(props) => <SelfForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
@ -120,6 +121,7 @@ class EntryPage extends React.Component {
<Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} /> <Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
<Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} /> <Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
<Route exact path="/qrcode/:owner/:paymentName" render={(props) => <QrCodePage {...this.props} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/qrcode/:owner/:paymentName" render={(props) => <QrCodePage {...this.props} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/captcha" render={(props) => <CaptchaPage {...props} />} />
</Switch> </Switch>
</div> </div>
</React.Fragment> </React.Fragment>

View File

@ -84,9 +84,11 @@ class GroupListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -199,7 +199,7 @@ class InvitationEditPage extends React.Component {
<Select virtual={false} style={{width: "100%"}} value={this.state.invitation.application} <Select virtual={false} style={{width: "100%"}} value={this.state.invitation.application}
onChange={(value => {this.updateInvitationField("application", value);})} onChange={(value => {this.updateInvitationField("application", value);})}
options={[ options={[
{label: "All", value: i18next.t("general:All")}, {label: i18next.t("general:All"), value: "All"},
...this.state.applications.map((application) => Setting.getOption(application.name, application.name)), ...this.state.applications.map((application) => Setting.getOption(application.name, application.name)),
]} /> ]} />
</Col> </Col>

View File

@ -68,9 +68,11 @@ class InvitationListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -13,12 +13,13 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd"; import {Button, Card, Col, Input, InputNumber, Row, Select, Space, Switch} from "antd";
import {EyeInvisibleOutlined, EyeTwoTone} from "@ant-design/icons"; import {EyeInvisibleOutlined, EyeTwoTone, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as LddpBackend from "./backend/LdapBackend"; import * as LddpBackend from "./backend/LdapBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as GroupBackend from "./backend/GroupBackend";
const {Option} = Select; const {Option} = Select;
@ -30,12 +31,14 @@ class LdapEditPage extends React.Component {
organizationName: props.match.params.organizationName, organizationName: props.match.params.organizationName,
ldap: null, ldap: null,
organizations: [], organizations: [],
groups: null,
}; };
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getLdap(); this.getLdap();
this.getOrganizations(); this.getOrganizations();
this.getGroups();
} }
getLdap() { getLdap() {
@ -60,6 +63,17 @@ class LdapEditPage extends React.Component {
}); });
} }
getGroups() {
GroupBackend.getGroups(this.state.organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
updateLdapField(key, value) { updateLdapField(key, value) {
this.setState((prevState) => { this.setState((prevState) => {
prevState.ldap[key] = value; prevState.ldap[key] = value;
@ -214,6 +228,31 @@ class LdapEditPage extends React.Component {
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
</Col>
<Col span={21}>
<Select virtual={false} style={{width: "100%"}} value={this.state.ldap.defaultGroup ?? []} onChange={(value => {
this.updateLdapField("defaultGroup", value);
})}
>
<Option key={""} value={""}>
<Space>
{i18next.t("general:Default")}
</Space>
</Option>
{
this.state.groups?.map((group) => <Option key={group.name} value={`${group.owner}/${group.name}`}>
<Space>
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
{group.displayName}
</Space>
</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}}> <Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}> <Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} : {Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} :

View File

@ -328,6 +328,8 @@ function ManagementPage(props) {
return <Redirect to="/login" />; return <Redirect to="/login" />;
} else if (props.account === undefined) { } else if (props.account === undefined) {
return null; return null;
} else if (props.account.needUpdatePassword) {
return <Redirect to={"/forget/" + props.application.name} />;
} else { } else {
return component; return component;
} }
@ -409,7 +411,7 @@ function ManagementPage(props) {
return Setting.isMobile() || window.location.pathname.startsWith("/trees"); return Setting.isMobile() || window.location.pathname.startsWith("/trees");
} }
const menuStyleRight = Setting.isAdminUser(props.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "280px"; const menuStyleRight = Setting.isAdminUser(props.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "320px";
const onClose = () => { const onClose = () => {
setMenuVisible(false); setMenuVisible(false);

View File

@ -72,9 +72,11 @@ class ModelListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -360,7 +360,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.defaultApplication} onChange={(value => {this.updateOrganizationField("defaultApplication", value);})} <Select virtual={false} style={{width: "100%"}} value={this.state.organization.defaultApplication} onChange={(value => {this.updateOrganizationField("defaultApplication", value);})}
options={this.state.applications?.map((item) => Setting.getOption(item.name, item.name)) options={this.state.applications?.map((item) => Setting.getOption(Setting.getApplicationDisplayName(item.name), item.name))
} /> } />
</Col> </Col>
</Row> </Row>
@ -436,6 +436,26 @@ class OrganizationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Use Email as username"), i18next.t("organization:Use Email as username - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.organization.useEmailAsUsername} onChange={checked => {
this.updateOrganizationField("useEmailAsUsername", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Enable tour"), i18next.t("general:Enable tour - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.organization.enableTour} onChange={checked => {
this.updateOrganizationField("enableTour", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} : {Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

View File

@ -44,6 +44,7 @@ class OrganizationListPage extends BaseListPage {
defaultPassword: "", defaultPassword: "",
enableSoftDeletion: false, enableSoftDeletion: false,
isProfilePublic: true, isProfilePublic: true,
enableTour: true,
accountItems: [ accountItems: [
{name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"}, {name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
@ -87,6 +88,7 @@ class OrganizationListPage extends BaseListPage {
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
], ],
}; };
} }
@ -113,11 +115,11 @@ class OrganizationListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i),
pagination: { pagination: {
...this.state.pagination, ...this.state.pagination,
total: this.state.pagination.total - 1}, current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
window.dispatchEvent(new Event("storageOrganizationsChanged")); window.dispatchEvent(new Event("storageOrganizationsChanged"));
} else { } else {

View File

@ -70,9 +70,11 @@ class PaymentListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -17,6 +17,7 @@ import {Button, Result, Spin} from "antd";
import * as PaymentBackend from "./backend/PaymentBackend"; import * as PaymentBackend from "./backend/PaymentBackend";
import * as PricingBackend from "./backend/PricingBackend"; import * as PricingBackend from "./backend/PricingBackend";
import * as SubscriptionBackend from "./backend/SubscriptionBackend"; import * as SubscriptionBackend from "./backend/SubscriptionBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
@ -34,6 +35,7 @@ class PaymentResultPage extends React.Component {
pricing: props.pricing ?? null, pricing: props.pricing ?? null,
subscription: props.subscription ?? null, subscription: props.subscription ?? null,
timeout: null, timeout: null,
user: null,
}; };
} }
@ -41,6 +43,25 @@ class PaymentResultPage extends React.Component {
this.getPayment(); this.getPayment();
} }
getUser() {
UserBackend.getUser(this.props.account.owner, this.props.account.name)
.then((res) => {
if (res.data === null) {
this.props.history.push("/404");
return;
}
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
user: res.data,
});
});
}
componentWillUnmount() { componentWillUnmount() {
if (this.state.timeout !== null) { if (this.state.timeout !== null) {
clearTimeout(this.state.timeout); clearTimeout(this.state.timeout);
@ -101,7 +122,7 @@ class PaymentResultPage extends React.Component {
payment: payment, payment: payment,
}); });
if (payment.state === "Created") { if (payment.state === "Created") {
if (["PayPal", "Stripe", "Alipay", "WeChat Pay"].includes(payment.type)) { if (["PayPal", "Stripe", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
this.setState({ this.setState({
timeout: setTimeout(async() => { timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName); await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
@ -114,6 +135,12 @@ class PaymentResultPage extends React.Component {
}); });
} }
} }
if (payment.state === "Paid") {
if (this.props.account) {
this.getUser();
}
}
} catch (err) { } catch (err) {
Setting.showMessage("error", err.message); Setting.showMessage("error", err.message);
return; return;
@ -136,6 +163,27 @@ class PaymentResultPage extends React.Component {
} }
if (payment.state === "Paid") { if (payment.state === "Paid") {
if (payment.isRecharge) {
return (
<div className="login-content">
{
Setting.renderHelmet(payment)
}
<Result
status="success"
title={`${i18next.t("payment:Recharged successfully")}`}
subTitle={`${i18next.t("payment:You have successfully recharged")} ${payment.price} ${Setting.getCurrencyText(payment)}, ${i18next.t("payment:Your current balance is")} ${this.state.user?.balance} ${Setting.getCurrencyText(payment)}`}
extra={[
<Button type="primary" key="returnUrl" onClick={() => {
this.goToPaymentUrl(payment);
}}>
{i18next.t("payment:Return to Website")}
</Button>,
]}
/>
</div>
);
}
return ( return (
<div className="login-content"> <div className="login-content">
{ {

View File

@ -487,6 +487,7 @@ class PermissionEditPage extends React.Component {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({ this.setState({
organizationName: this.state.permission.owner,
permissionName: this.state.permission.name, permissionName: this.state.permission.name,
}); });

View File

@ -69,9 +69,11 @@ class PermissionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -63,9 +63,11 @@ class PlanListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -59,9 +59,11 @@ class PricingListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Descriptions, Spin} from "antd"; import {Button, Descriptions, InputNumber, Space, Spin} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend"; import * as ProductBackend from "./backend/ProductBackend";
import * as PlanBackend from "./backend/PlanBackend"; import * as PlanBackend from "./backend/PlanBackend";
@ -36,6 +36,7 @@ class ProductBuyPage extends React.Component {
pricing: props?.pricing ?? null, pricing: props?.pricing ?? null,
plan: null, plan: null,
isPlacingOrder: false, isPlacingOrder: false,
customPrice: 0,
}; };
} }
@ -127,18 +128,8 @@ class ProductBuyPage extends React.Component {
} }
} }
getCurrencyText(product) {
if (product?.currency === "USD") {
return i18next.t("product:USD");
} else if (product?.currency === "CNY") {
return i18next.t("product:CNY");
} else {
return "(Unknown currency)";
}
}
getPrice(product) { getPrice(product) {
return `${this.getCurrencySymbol(product)}${product?.price} (${this.getCurrencyText(product)})`; return `${this.getCurrencySymbol(product)}${product?.price} (${Setting.getCurrencyText(product)})`;
} }
// Call Weechat Pay via jsapi // Call Weechat Pay via jsapi
@ -192,7 +183,7 @@ class ProductBuyPage extends React.Component {
isPlacingOrder: true, isPlacingOrder: true,
}); });
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "", this.state.paymentEnv) ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "", this.state.paymentEnv, this.state.customPrice)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const payment = res.data; const payment = res.data;
@ -295,15 +286,27 @@ class ProductBuyPage extends React.Component {
<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")}> {
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}> product.isRecharge ? (
{ <Descriptions.Item span={3} label={i18next.t("product:Price")}>
this.getPrice(product) <Space>
} <InputNumber min={0} value={this.state.customPrice} onChange={(e) => {this.setState({customPrice: e});}} /> {Setting.getCurrencyText(product)}
</span> </Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Quantity")}><span style={{fontSize: 16}}>{product?.quantity}</span></Descriptions.Item> ) : (
<Descriptions.Item label={i18next.t("product:Sold")}><span style={{fontSize: 16}}>{product?.sold}</span></Descriptions.Item> <React.Fragment>
<Descriptions.Item label={i18next.t("product:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
{
this.getPrice(product)
}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Quantity")}><span style={{fontSize: 16}}>{product?.quantity}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Sold")}><span style={{fontSize: 16}}>{product?.sold}</span></Descriptions.Item>
</React.Fragment>
)
}
<Descriptions.Item label={i18next.t("product:Pay")} span={3}> <Descriptions.Item label={i18next.t("product:Pay")} span={3}>
{ {
this.renderPay(product) this.renderPay(product)

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, Switch} 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";
@ -216,14 +216,27 @@ class ProductEditPage extends React.Component {
</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:Is recharge"), i18next.t("product:Is recharge - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.price} disabled={isCreatedByPlan} onChange={value => { <Switch checked={this.state.product.isRecharge} onChange={value => {
this.updateProductField("price", value); this.updateProductField("isRecharge", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
{
this.state.product.isRecharge ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.price} disabled={isCreatedByPlan} onChange={value => {
this.updateProductField("price", value);
}} />
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} : {Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :

View File

@ -38,6 +38,7 @@ class ProductListPage extends BaseListPage {
price: 300, price: 300,
quantity: 99, quantity: 99,
sold: 10, sold: 10,
isRecharge: false,
providers: [], providers: [],
state: "Published", state: "Published",
}; };
@ -64,9 +65,11 @@ class ProductListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -209,8 +209,6 @@ class ProviderEditPage extends React.Component {
return Setting.getLabel(i18next.t("provider:Public key"), i18next.t("provider:Public key - Tooltip")); return Setting.getLabel(i18next.t("provider:Public key"), i18next.t("provider:Public key - Tooltip"));
} else if (provider.type === "Msg91 SMS" || provider.type === "Infobip SMS" || provider.type === "OSON SMS") { } else if (provider.type === "Msg91 SMS" || provider.type === "Infobip SMS" || provider.type === "OSON SMS") {
return Setting.getLabel(i18next.t("provider:Sender Id"), i18next.t("provider:Sender Id - Tooltip")); return Setting.getLabel(i18next.t("provider:Sender Id"), i18next.t("provider:Sender Id - Tooltip"));
} else if (provider.type === "SendCloud SMS") {
return "SMS_USER";
} else { } else {
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip")); return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
} }
@ -262,8 +260,6 @@ class ProviderEditPage extends React.Component {
return Setting.getLabel(i18next.t("provider:Auth Key"), i18next.t("provider:Auth Key - Tooltip")); return Setting.getLabel(i18next.t("provider:Auth Key"), i18next.t("provider:Auth Key - Tooltip"));
} else if (provider.type === "Infobip SMS") { } else if (provider.type === "Infobip SMS") {
return Setting.getLabel(i18next.t("provider:Api Key"), i18next.t("provider:Api Key - Tooltip")); return Setting.getLabel(i18next.t("provider:Api Key"), i18next.t("provider:Api Key - Tooltip"));
} else if (provider.type === "SendCloud SMS") {
return "SMS_KEY";
} else { } else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
} }
@ -729,7 +725,7 @@ class ProviderEditPage extends React.Component {
(this.state.provider.category === "Web3") || (this.state.provider.category === "Web3") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") || (this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") || (this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP")) ? null : ( (this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
<React.Fragment> <React.Fragment>
{ {
(this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") || (this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") ||
@ -847,7 +843,7 @@ class ProviderEditPage extends React.Component {
) )
} }
{ {
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : ( this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.category !== "Storage") && this.state.provider.type !== "Okta" ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -874,7 +870,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} : {Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@ -889,7 +885,9 @@ class ProviderEditPage extends React.Component {
{["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} : {["Casdoor"].includes(this.state.provider.type) ?
Setting.getLabel(i18next.t("general:Provider"), i18next.t("provider:Provider - Tooltip"))
: Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.provider.bucket} onChange={e => { <Input value={this.state.provider.bucket} onChange={e => {
@ -910,7 +908,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -922,10 +920,24 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? ( {["Casdoor"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.content} onChange={e => {
this.updateProviderField("content", e.target.value);
}} />
</Col>
</Row>
) : null}
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ?
Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip")) :
Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.provider.regionId} onChange={e => { <Input value={this.state.provider.regionId} onChange={e => {
@ -1107,7 +1119,7 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) : this.state.provider.category === "SMS" ? ( ) : this.state.provider.category === "SMS" ? (
<React.Fragment> <React.Fragment>
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS", "SendCloud SMS"].includes(this.state.provider.type) ? {["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
null : 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}>
@ -1302,7 +1314,7 @@ class ProviderEditPage extends React.Component {
) : null ) : null
} }
{ {
(this.state.provider.type === "Alipay" || this.state.provider.type === "WeChat Pay") ? ( (this.state.provider.type === "Alipay" || this.state.provider.type === "WeChat Pay" || this.state.provider.type === "Casdoor") ? (
<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"))} :

View File

@ -76,9 +76,11 @@ class ProviderListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -40,9 +40,11 @@ class ResourceListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -61,9 +61,11 @@ class RoleListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -27,9 +27,11 @@ class SessionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -56,6 +56,8 @@ export const Countries = [
{label: "Українська", key: "uk", country: "UA", alt: "Українська"}, {label: "Українська", key: "uk", country: "UA", alt: "Українська"},
{label: "Қазақ", key: "kk", country: "KZ", alt: "Қазақ"}, {label: "Қазақ", key: "kk", country: "KZ", alt: "Қазақ"},
{label: "فارسی", key: "fa", country: "IR", alt: "فارسی"}, {label: "فارسی", key: "fa", country: "IR", alt: "فارسی"},
{label: "Čeština", key: "cs", country: "CZ", alt: "Čeština"},
{label: "Slovenčina", key: "sk", country: "SK", alt: "Slovenčina"},
]; ];
export function getThemeData(organization, application) { export function getThemeData(organization, application) {
@ -139,10 +141,6 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_twilio.svg`, logo: `${StaticBaseUrl}/img/social_twilio.svg`,
url: "https://www.twilio.com/messaging", url: "https://www.twilio.com/messaging",
}, },
"SendCloud SMS": {
logo: `${StaticBaseUrl}/img/sms_sendcloud.png`,
url: "https://www.sendcloud.net/",
},
"SmsBao SMS": { "SmsBao SMS": {
logo: `${StaticBaseUrl}/img/social_smsbao.png`, logo: `${StaticBaseUrl}/img/social_smsbao.png`,
url: "https://www.smsbao.com/", url: "https://www.smsbao.com/",
@ -231,6 +229,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_synology.png`, logo: `${StaticBaseUrl}/img/social_synology.png`,
url: "https://www.synology.com/en-global/dsm/feature/file_sharing", url: "https://www.synology.com/en-global/dsm/feature/file_sharing",
}, },
"Casdoor": {
logo: `${StaticBaseUrl}/img/casdoor.png`,
url: "https://casdoor.org/docs/provider/storage/overview",
},
}, },
SAML: { SAML: {
"Aliyun IDaaS": { "Aliyun IDaaS": {
@ -251,6 +253,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/payment_paypal.png`, logo: `${StaticBaseUrl}/img/payment_paypal.png`,
url: "", url: "",
}, },
"Balance": {
logo: `${StaticBaseUrl}/img/payment_balance.svg`,
url: "",
},
"Alipay": { "Alipay": {
logo: `${StaticBaseUrl}/img/payment_alipay.png`, logo: `${StaticBaseUrl}/img/payment_alipay.png`,
url: "https://www.alipay.com/", url: "https://www.alipay.com/",
@ -281,6 +287,14 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`, logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha", url: "https://www.google.com/recaptcha",
}, },
"reCAPTCHA v2": {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha",
},
"reCAPTCHA v3": {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha",
},
"hCaptcha": { "hCaptcha": {
logo: `${StaticBaseUrl}/img/social_hcaptcha.png`, logo: `${StaticBaseUrl}/img/social_hcaptcha.png`,
url: "https://www.hcaptcha.com", url: "https://www.hcaptcha.com",
@ -1043,7 +1057,6 @@ export function getProviderTypeOptions(category) {
{id: "Huawei Cloud SMS", name: "Huawei Cloud SMS"}, {id: "Huawei Cloud SMS", name: "Huawei Cloud SMS"},
{id: "UCloud SMS", name: "UCloud SMS"}, {id: "UCloud SMS", name: "UCloud SMS"},
{id: "Twilio SMS", name: "Twilio SMS"}, {id: "Twilio SMS", name: "Twilio SMS"},
{id: "SendCloud SMS", name: "SendCloud SMS"},
{id: "SmsBao SMS", name: "SmsBao SMS"}, {id: "SmsBao SMS", name: "SmsBao SMS"},
{id: "SUBMAIL SMS", name: "SUBMAIL SMS"}, {id: "SUBMAIL SMS", name: "SUBMAIL SMS"},
{id: "Msg91 SMS", name: "Msg91 SMS"}, {id: "Msg91 SMS", name: "Msg91 SMS"},
@ -1061,6 +1074,7 @@ export function getProviderTypeOptions(category) {
{id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"}, {id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"},
{id: "Google Cloud Storage", name: "Google Cloud Storage"}, {id: "Google Cloud Storage", name: "Google Cloud Storage"},
{id: "Synology", name: "Synology"}, {id: "Synology", name: "Synology"},
{id: "Casdoor", name: "Casdoor"},
] ]
); );
} else if (category === "SAML") { } else if (category === "SAML") {
@ -1072,6 +1086,7 @@ export function getProviderTypeOptions(category) {
} else if (category === "Payment") { } else if (category === "Payment") {
return ([ return ([
{id: "Dummy", name: "Dummy"}, {id: "Dummy", name: "Dummy"},
{id: "Balance", name: "Balance"},
{id: "Alipay", name: "Alipay"}, {id: "Alipay", name: "Alipay"},
{id: "WeChat Pay", name: "WeChat Pay"}, {id: "WeChat Pay", name: "WeChat Pay"},
{id: "PayPal", name: "PayPal"}, {id: "PayPal", name: "PayPal"},
@ -1081,7 +1096,8 @@ export function getProviderTypeOptions(category) {
} else if (category === "Captcha") { } else if (category === "Captcha") {
return ([ return ([
{id: "Default", name: "Default"}, {id: "Default", name: "Default"},
{id: "reCAPTCHA", name: "reCAPTCHA"}, {id: "reCAPTCHA v2", name: "reCAPTCHA v2"},
{id: "reCAPTCHA v3", name: "reCAPTCHA v3"},
{id: "hCaptcha", name: "hCaptcha"}, {id: "hCaptcha", name: "hCaptcha"},
{id: "Aliyun Captcha", name: "Aliyun Captcha"}, {id: "Aliyun Captcha", name: "Aliyun Captcha"},
{id: "GEETEST", name: "GEETEST"}, {id: "GEETEST", name: "GEETEST"},
@ -1369,6 +1385,13 @@ export function getApplicationName(application) {
return `${application?.owner}/${application?.name}`; return `${application?.owner}/${application?.name}`;
} }
export function getApplicationDisplayName(application) {
if (application.isShared) {
return `${application.name}(Shared)`;
}
return application.name;
}
export function getRandomName() { export function getRandomName() {
return Math.random().toString(36).slice(-6); return Math.random().toString(36).slice(-6);
} }
@ -1468,7 +1491,7 @@ export function getUserCommonFields() {
return ["Owner", "Name", "CreatedTime", "UpdatedTime", "DeletedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar", return ["Owner", "Name", "CreatedTime", "UpdatedTime", "DeletedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region", "Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
"Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp", "Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp",
"PreferredMfaType", "TotpSecret", "SignupApplication"]; "PreferredMfaType", "TotpSecret", "SignupApplication", "RecoveryCodes", "MfaPhoneEnabled", "MfaEmailEnabled"];
} }
export function getDefaultFooterContent() { export function getDefaultFooterContent() {
@ -1521,3 +1544,13 @@ export function getDefaultHtmlEmailContent() {
</body> </body>
</html>`; </html>`;
} }
export function getCurrencyText(product) {
if (product?.currency === "USD") {
return i18next.t("product:USD");
} else if (product?.currency === "CNY") {
return i18next.t("product:CNY");
} else {
return "(Unknown currency)";
}
}

View File

@ -64,9 +64,11 @@ class SubscriptionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -69,9 +69,11 @@ class SyncerListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -61,9 +61,11 @@ class TokenListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -203,13 +203,24 @@ export function getNextUrl(pathName = window.location.pathname) {
return TourUrlList[TourUrlList.indexOf(pathName.replace("/", "")) + 1] || ""; return TourUrlList[TourUrlList.indexOf(pathName.replace("/", "")) + 1] || "";
} }
let orgIsTourVisible = true;
export function setOrgIsTourVisible(visible) {
orgIsTourVisible = visible;
}
export function setIsTourVisible(visible) { export function setIsTourVisible(visible) {
localStorage.setItem("isTourVisible", visible); localStorage.setItem("isTourVisible", visible);
window.dispatchEvent(new Event("storageTourChanged")); }
export function setTourLogo(tourLogoSrc) {
if (tourLogoSrc !== "") {
TourObj["home"][0]["cover"] = (<img alt="casdoor.png" src={tourLogoSrc} />);
}
} }
export function getTourVisible() { export function getTourVisible() {
return localStorage.getItem("isTourVisible") !== "false"; return localStorage.getItem("isTourVisible") !== "false" && orgIsTourVisible;
} }
export function getNextButtonChild(nextPathName) { export function getNextButtonChild(nextPathName) {

View File

@ -125,7 +125,7 @@ class TransactionEditPage extends React.Component {
application: application, application: application,
}); });
this.getCerts(application.organization); this.getCerts(application);
this.getSamlMetadata(application.enableSamlPostBinding); this.getSamlMetadata(application.enableSamlPostBinding);
}); });

View File

@ -54,9 +54,11 @@ class TransactionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -41,6 +41,7 @@ import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-de
import * as MfaBackend from "./backend/MfaBackend"; import * as MfaBackend from "./backend/MfaBackend";
import AccountAvatar from "./account/AccountAvatar"; import AccountAvatar from "./account/AccountAvatar";
import FaceIdTable from "./table/FaceIdTable"; import FaceIdTable from "./table/FaceIdTable";
import MfaAccountTable from "./table/MfaAccountTable";
const {Option} = Select; const {Option} = Select;
@ -211,6 +212,9 @@ class UserEditPage extends React.Component {
const user = this.state.user; const user = this.state.user;
if (key === "address") { if (key === "address") {
if (!user[key]) {
user[key] = ["", ""];
}
user[key][idx] = value; user[key][idx] = value;
} else { } else {
user[key] = value; user[key] = value;
@ -515,7 +519,7 @@ class UserEditPage extends React.Component {
<span>{i18next.t("user:Address line") + " 1"}</span> : <span>{i18next.t("user:Address line") + " 1"}</span> :
</Col> </Col>
<Col span={20} > <Col span={20} >
<Input value={this.state.user.address[0]} onChange={e => { <Input value={!this.state.user.address ? "" : this.state.user.address[0]} onChange={e => {
this.updateUserField("address", e.target.value, 0); this.updateUserField("address", e.target.value, 0);
}} /> }} />
</Col> </Col>
@ -527,7 +531,7 @@ class UserEditPage extends React.Component {
<span>{i18next.t("user:Address line") + " 2"}</span> : <span>{i18next.t("user:Address line") + " 2"}</span> :
</Col> </Col>
<Col span={20} > <Col span={20} >
<Input value={this.state.user.address[1]} onChange={e => { <Input value={!this.state.user.address ? "" : this.state.user.address[1]} onChange={e => {
this.updateUserField("address", e.target.value, 1); this.updateUserField("address", e.target.value, 1);
}} /> }} />
</Col> </Col>
@ -704,6 +708,19 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "Balance") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Balance"), i18next.t("user:Balance - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.user.balance} onChange={value => {
this.updateUserField("balance", value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Score") { } else if (accountItem.name === "Score") {
return ( return (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@ -1023,6 +1040,34 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "MFA accounts") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:MFA accounts"), i18next.t("user:MFA accounts"))} :
</Col>
<Col span={22} >
<MfaAccountTable
title={i18next.t("user:MFA accounts")}
table={this.state.user.mfaAccounts}
onUpdateTable={(table) => {this.updateUserField("mfaAccounts", table);}}
/>
</Col>
</Row>
);
} else if (accountItem.name === "Need update password") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Need update password"), i18next.t("user:Need update password - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch disabled={(!this.state.user.phone) && (!this.state.user.email) && (!this.state.user.mfaProps)} checked={this.state.user.needUpdatePassword} onChange={checked => {
this.updateUserField("needUpdatePassword", checked);
}} />
</Col>
</Row>
);
} }
} }

View File

@ -110,9 +110,11 @@ class UserListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -167,6 +167,9 @@ class WebhookEditPage extends React.Component {
["add", "update", "delete"].forEach(action => { ["add", "update", "delete"].forEach(action => {
res.push(`${action}-${obj}`); res.push(`${action}-${obj}`);
}); });
if (obj === "payment") {
res.push("invoice-payment", "notify-payment");
}
}); });
return res; return res;
} }

View File

@ -61,9 +61,11 @@ class WebhookListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

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