Compare commits

...

73 Commits

Author SHA1 Message Date
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
af2a9f0374 feat: get phone number and country from Google OAuth provider (#2965)
* feat: get phone number and country from Google OAuth provider

* feat: fix i18n
2024-05-23 00:42:36 +08:00
bfcfb56336 feat: add address line 1 and 2 in web UI (#2961) 2024-05-19 23:55:38 +08:00
c48306d117 feat: check signup item email regex in signup page (#2960)
* feat: check email regex in frontend

* Update SignupPage.js

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-05-19 22:07:34 +08:00
6efec6b4b5 feat: support "label" field for signin item table (#2956) 2024-05-19 03:07:36 +08:00
2daf26aa88 feat: use lowercase username when isUsernameLowered is enabled (#2952)
* feat: auto trim username during login and lowercase when isUsernameLowered enabled in conf

* fix: fix linter error

* fix: fix linter error

* fix: fix linter error
2024-05-17 11:43:19 +08:00
21c151bcf8 feat: fix password not updated bug when updating syncer (#2945) 2024-05-13 00:12:35 +08:00
b6b0b7d318 feat: support checking whether send-webhook API has error (#2944)
* feat: add webhook response for record

* refactor: refactor SendWebhook and use readall to read response body

* fix: improve code format

* fix: improve code format

* fix: improve code format
2024-05-12 20:30:15 +08:00
0ecc1d599f feat: fix bug in AddUsersInBatch() 2024-05-11 16:59:33 +08:00
3456fc6695 fix: update go-sms-sender to v0.23.0 2024-05-10 14:05:53 +08:00
c302dc7b8e fix: fix bug when init plan and pricing and record (#2934)
* fix: fix potential bugs in init data

* fix: improve code format

* fix: fix bug when init plan and pricing and record
2024-05-07 23:33:01 +08:00
d24ddd4f1c feat: fix potential bugs in init_data.go (#2932)
* fix: fix potential bugs in init data

* fix: improve code format
2024-05-07 23:11:08 +08:00
572616d390 fix: fix bug in ProviderItem.CountryCodes 2024-05-07 17:17:45 +08:00
2187310dbc feat: fix bug in initDefinedOrganization() 2024-05-06 13:57:08 +08:00
26345bb21b feat: add sms provider sendcloud (#2927) 2024-05-06 13:38:55 +08:00
e0455df504 feat: improve record content masking (#2923)
* feat: hide password in record

* feat: improve code format

* feat: improve code format
2024-05-05 12:42:09 +08:00
1dfbbf0e90 feat: fix bug that fails to import built-in org via init_data.json (#2922) 2024-05-05 01:06:15 +08:00
d43d58dee2 feat: fix getProviders() owner bug in product edit page 2024-05-01 18:04:50 +08:00
9eb4b12041 fix: rename to countryCodes for UI 2024-05-01 11:44:21 +08:00
3a45a4ee77 fix: rename to countryCodes 2024-05-01 09:47:44 +08:00
43393f034b feat: fix the Email provider fails to match bug in GetProviderByCategoryAndRule() 2024-05-01 09:44:19 +08:00
bafa80513b fix: improve ProviderTable column UI 2024-05-01 00:46:48 +08:00
8d08140421 fix: fix typo in initBuiltInPermission() 2024-05-01 00:41:16 +08:00
3d29e27d54 feat: support multiple SMS providers for different regions (#2914)
* feat: support using different sms provider for different region

* feat: add multiple support for select and remove log

* feat: revert change for countryCode in loginPage

* feat: revert change for countryCode in user_util.go

* feat: revert change for countryCode in auth.go

* Update application_item.go

* Update CountryCodeSelect.js

* Update ProviderTable.js

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-05-01 00:40:47 +08:00
199f1d4d10 feat: fix Auto-login causing AuthCodeWithPKCE Failures (#2911) 2024-04-30 12:14:50 +08:00
227e938db6 feat: fix error behavior of custom oauth/saml provider in login page in big icon mode (#2900) 2024-04-26 23:33:41 +08:00
739cfd84ed feat: cannot empty SigninMethodTable now 2024-04-26 21:23:23 +08:00
8dbb041a34 feat: fix empty custom CSS for new rows in signin items table (#2897) 2024-04-24 15:19:30 +08:00
af2d26daf2 Add object.IsAppUser() 2024-04-24 01:10:38 +08:00
90d502ab2b feat: add custom css style for signup page and enhance css edit (#2880)
* feat: add custom css style for signup page and enhance css edit in signintable

* feat: change cssStyle to customCss

* feat: auto hide <style> label, fix display problem on providers, remove auto add providers in signup page

* fix: fix indent in signin items customCss and fix providers display in signup items

* fix: fix login replace logical
2024-04-21 11:56:18 +08:00
d51af3378e fix: fix init data not saved to database (#2885) (#2886) 2024-04-21 11:55:06 +08:00
87e2b97813 feat: translate Ukrainian language i18n 2024-04-20 02:14:23 +08:00
d9e44c1f2d fix: add "Is used" to verification list page 2024-04-20 00:18:52 +08:00
dfa4503f24 feat: support "mfa_phone_enabled", "mfa_email_enabled" in update-user API 2024-04-20 00:16:45 +08:00
f7fb32893b fix: close file in LocalFileSystemProvider's Put() (#2882) 2024-04-20 00:11:52 +08:00
66d0758b13 feat: fix DisableVerificationCode bug about empty email and phone 2024-04-19 13:28:13 +08:00
46ad0fe0be Improve Email Send() logic 2024-04-11 19:09:48 +08:00
6b637e3b2e feat: fix SendgridEmailProvider error handling, fix send-email template 2024-04-11 00:18:39 +08:00
3354945119 feat: add SendGrid Email provider (#2865)
* feat: add support for email provider send grid

* feat: rename send grid to sendgrid

* feat: rename send grid to sendgrid

* feat: change logo url of send grid
2024-04-09 22:16:01 +08:00
19c4416f10 feat: degrade the ant-design/cssinjs version to fix the Chrome 87 broken UI issue (#2861) 2024-04-09 09:15:39 +08:00
2077db9091 fix: fix bug in VerificationListPage 2024-04-07 15:39:25 +08:00
800f0ed249 feat: add tzdata package in Dockerfile to fix timezone issue (#2857)
Add tzdata to resolve possible time zone errors
2024-04-07 14:27:45 +08:00
xyt
6161040c67 fix: Dismiss google one tap after logged in by setting disableCancelOnUnmount to false (#2854)
* fix: Google One Tap should be hidden after logged in

* Change the call location for google.accounts.id.cancel()

* fix: hide google one tap after login by set disableCancelOnUnmount to false
2024-04-05 23:39:33 +08:00
xyt
1d785e61c6 feat: Google One Tap should be hidden after logged in (#2853)
* fix: Google One Tap should be hidden after logged in

* Change the call location for google.accounts.id.cancel()
2024-04-05 20:10:13 +08:00
0329d24867 feat: add isUsernameLowered to config 2024-04-02 21:54:16 +08:00
fb6f3623ee feat: add requireProviderPermission() 2024-03-30 23:24:59 +08:00
eb448bd043 fix: fix permission problem in provider (#2848) 2024-03-30 23:18:03 +08:00
xyt
ea88839db9 feat: add back button in forget password page (#2847)
* feat: add back button in forget password page

* fix: can't step back when directly entering forgot password page

* feat: forget password page always return to login page

* feat: if has history then go back to history & change style

* Update ForgetPage.js

* fix: reset button position

* Update ForgetPage.js

* Update ForgetPage.js

---------

Co-authored-by: Eric Luo <hsluoyz@qq.com>
2024-03-30 23:17:47 +08:00
cb95f6977a fix: fix PasswordModal error when changing username 2024-03-30 12:28:55 +08:00
9067df92a7 feat: revert "feat: Support metamask mobile login" (#2845)
This reverts commit bfa2ab63ad.
2024-03-30 00:36:25 +08:00
bfa2ab63ad feat: Support metamask mobile login (#2844) 2024-03-30 00:08:52 +08:00
505054b0eb feat: use minWidth for a better display effect in org select (#2843) 2024-03-29 15:47:27 +08:00
f95ce13b82 fix: support "Email or Phone" in signup table 2024-03-29 09:07:37 +08:00
126 changed files with 3400 additions and 1766 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,9 +13,13 @@ 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
RUN apk add tzdata
RUN apk add curl RUN apk add curl
RUN apk add ca-certificates && update-ca-certificates RUN apk add ca-certificates && update-ca-certificates
@ -27,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
@ -46,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

@ -15,6 +15,7 @@ socks5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10 verificationCodeTimeout = 10
initScore = 0 initScore = 0
logPostOnly = true logPostOnly = true
isUsernameLowered = false
origin = origin =
originFrontend = originFrontend =
staticBaseUrl = "https://cdn.casbin.org" staticBaseUrl = "https://cdn.casbin.org"

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()
@ -261,16 +265,20 @@ func (c *ApiController) Signup() {
c.SetSessionUsername(user.GetId()) c.SetSessionUsername(user.GetId())
} }
err = object.DisableVerificationCode(authForm.Email) if authForm.Email != "" {
if err != nil { err = object.DisableVerificationCode(authForm.Email)
c.ResponseError(err.Error()) if err != nil {
return c.ResponseError(err.Error())
return
}
} }
err = object.DisableVerificationCode(checkPhone) if checkPhone != "" {
if err != nil { err = object.DisableVerificationCode(checkPhone)
c.ResponseError(err.Error()) if err != nil {
return c.ResponseError(err.Error())
return
}
} }
c.Ctx.Input.SetParam("recordUserId", user.GetId()) c.Ctx.Input.SetParam("recordUserId", user.GetId())

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

View File

@ -73,7 +73,7 @@ func (c *ApiController) IsAdminOrSelf(user2 *object.User) bool {
func (c *ApiController) isGlobalAdmin() (bool, *object.User) { func (c *ApiController) isGlobalAdmin() (bool, *object.User) {
username := c.GetSessionUsername() username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") { if object.IsAppUser(username) {
// e.g., "app/app-casnode" // e.g., "app/app-casnode"
return true, nil return true, nil
} }

View File

@ -68,7 +68,7 @@ func (c *ApiController) GetCerts() {
// GetGlobalCerts // GetGlobalCerts
// @Title GetGlobalCerts // @Title GetGlobalCerts
// @Tag Cert API // @Tag Cert API
// @Description get globle certs // @Description get global certs
// @Success 200 {array} object.Cert The Response object // @Success 200 {array} object.Cert The Response object
// @router /get-global-certs [get] // @router /get-global-certs [get]
func (c *ApiController) GetGlobalCerts() { func (c *ApiController) GetGlobalCerts() {

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

@ -141,6 +141,20 @@ func (c *ApiController) GetProvider() {
c.ResponseOk(object.GetMaskedProvider(provider, isMaskEnabled)) c.ResponseOk(object.GetMaskedProvider(provider, isMaskEnabled))
} }
func (c *ApiController) requireProviderPermission(provider *object.Provider) bool {
isGlobalAdmin, user := c.isGlobalAdmin()
if isGlobalAdmin {
return true
}
if provider.Owner == "admin" || user.Owner != provider.Owner {
c.ResponseError(c.T("auth:Unauthorized operation"))
return false
}
return true
}
// UpdateProvider // UpdateProvider
// @Title UpdateProvider // @Title UpdateProvider
// @Tag Provider API // @Tag Provider API
@ -159,6 +173,11 @@ func (c *ApiController) UpdateProvider() {
return return
} }
ok := c.requireProviderPermission(&provider)
if !ok {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateProvider(id, &provider)) c.Data["json"] = wrapActionResponse(object.UpdateProvider(id, &provider))
c.ServeJSON() c.ServeJSON()
} }
@ -184,11 +203,17 @@ func (c *ApiController) AddProvider() {
return return
} }
if err := checkQuotaForProvider(int(count)); err != nil { err = checkQuotaForProvider(int(count))
if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
ok := c.requireProviderPermission(&provider)
if !ok {
return
}
c.Data["json"] = wrapActionResponse(object.AddProvider(&provider)) c.Data["json"] = wrapActionResponse(object.AddProvider(&provider))
c.ServeJSON() c.ServeJSON()
} }
@ -208,6 +233,11 @@ func (c *ApiController) DeleteProvider() {
return return
} }
ok := c.requireProviderPermission(&provider)
if !ok {
return
}
c.Data["json"] = wrapActionResponse(object.DeleteProvider(&provider)) c.Data["json"] = wrapActionResponse(object.DeleteProvider(&provider))
c.ServeJSON() c.ServeJSON()
} }

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)
@ -113,25 +120,25 @@ func (c *ApiController) SendEmail() {
content := emailForm.Content content := emailForm.Content
if content == "" { if content == "" {
code := "123456" content = provider.Content
}
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes." code := "123456"
content = strings.Replace(provider.Content, "%s", code, 1) // "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
if !strings.HasPrefix(userId, "app/") { content = strings.Replace(content, "%s", code, 1)
var user *object.User userString := "Hi"
user, err = object.GetUser(userId) if !object.IsAppUser(userId) {
if err != nil { var user *object.User
c.ResponseError(err.Error()) user, err = object.GetUser(userId)
return if err != nil {
} c.ResponseError(err.Error())
return
userString := "Hi" }
if user != nil { if user != nil {
userString = user.GetFriendlyName() userString = user.GetFriendlyName()
}
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
} }
} }
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
for _, receiver := range emailForm.Receivers { for _, receiver := range emailForm.Receivers {
err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender) err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender)

View File

@ -20,6 +20,7 @@ import (
"strings" "strings"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -293,6 +294,11 @@ func (c *ApiController) UpdateUser() {
return return
} }
isUsernameLowered := conf.GetConfigBool("isUsernameLowered")
if isUsernameLowered {
user.Name = strings.ToLower(user.Name)
}
isAdmin := c.IsAdmin() isAdmin := c.IsAdmin()
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, c.GetAcceptLanguage()); !pass { if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, c.GetAcceptLanguage()); !pass {
c.ResponseError(err) c.ResponseError(err)
@ -503,8 +509,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

@ -96,7 +96,7 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
return nil, false return nil, false
} }
if strings.HasPrefix(userId, "app/") { if object.IsAppUser(userId) {
tmpUserId := c.Input().Get("userId") tmpUserId := c.Input().Get("userId")
if tmpUserId != "" { if tmpUserId != "" {
userId = tmpUserId userId = tmpUserId
@ -142,7 +142,7 @@ func (c *ApiController) IsOrgAdmin() (bool, bool) {
return false, true return false, true
} }
if strings.HasPrefix(userId, "app/") { if object.IsAppUser(userId) {
return true, true return true, true
} }

View File

@ -295,7 +295,7 @@ func (c *ApiController) SendVerificationCode() {
vform.CountryCode = mfaProps.CountryCode vform.CountryCode = mfaProps.CountryCode
} }
provider, err = application.GetSmsProvider(vform.Method) provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -111,46 +111,44 @@ func newEmail(fromAddress string, toAddress string, subject string, content stri
Subject: subject, Subject: subject,
HTML: content, HTML: content,
}, },
Importance: importanceNormal, Importance: importanceNormal,
Attachments: []Attachment{},
} }
} }
func (a *AzureACSEmailProvider) sendEmail(e *Email) error { func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
postBody, err := json.Marshal(e) email := newEmail(fromAddress, toAddress, subject, content)
if err != nil {
return fmt.Errorf("email JSON marshall failed: %s", err)
}
bodyBuffer := bytes.NewBuffer(postBody) postBody, err := json.Marshal(email)
if err != nil {
return err
}
endpoint := strings.TrimSuffix(a.Endpoint, "/") endpoint := strings.TrimSuffix(a.Endpoint, "/")
url := fmt.Sprintf("%s/emails:send?api-version=2023-03-31", endpoint) url := fmt.Sprintf("%s/emails:send?api-version=2023-03-31", endpoint)
bodyBuffer := bytes.NewBuffer(postBody)
req, err := http.NewRequest("POST", url, bodyBuffer) req, err := http.NewRequest("POST", url, bodyBuffer)
if err != nil { if err != nil {
return fmt.Errorf("error creating AzureACS API request: %s", err) return err
} }
// Sign the request using the AzureACS access key and HMAC-SHA256
err = signRequestHMAC(a.AccessKey, req) err = signRequestHMAC(a.AccessKey, req)
if err != nil { if err != nil {
return fmt.Errorf("error signing AzureACS API request: %s", err) return err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
// Some important header
req.Header.Set("repeatability-request-id", uuid.New().String()) req.Header.Set("repeatability-request-id", uuid.New().String())
req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat)) req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat))
// Send request
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("error sending AzureACS API request: %s", err) return err
} }
defer resp.Body.Close() defer resp.Body.Close()
// Response error Handling
if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized { if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized {
commError := ErrorResponse{} commError := ErrorResponse{}
@ -159,11 +157,11 @@ func (a *AzureACSEmailProvider) sendEmail(e *Email) error {
return err return err
} }
return fmt.Errorf("error sending email: %s", commError.Error.Message) return fmt.Errorf("status code: %d, error message: %s", resp.StatusCode, commError.Error.Message)
} }
if resp.StatusCode != http.StatusAccepted { if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("error sending email: status: %d", resp.StatusCode) return fmt.Errorf("status code: %d", resp.StatusCode)
} }
return nil return nil
@ -221,9 +219,3 @@ func GetHmac(content string, key []byte) string {
return base64.StdEncoding.EncodeToString(hmac.Sum(nil)) return base64.StdEncoding.EncodeToString(hmac.Sum(nil))
} }
func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
e := newEmail(fromAddress, toAddress, subject, content)
return a.sendEmail(e)
}

View File

@ -23,6 +23,8 @@ func GetEmailProvider(typ string, clientId string, clientSecret string, host str
return NewAzureACSEmailProvider(clientSecret, host) return NewAzureACSEmailProvider(clientSecret, host)
} else if typ == "Custom HTTP Email" { } else if typ == "Custom HTTP Email" {
return NewHttpEmailProvider(endpoint, method) return NewHttpEmailProvider(endpoint, method)
} else if typ == "SendGrid" {
return NewSendgridEmailProvider(clientSecret)
} else { } else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl) return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
} }

68
email/sendgrid.go Normal file
View File

@ -0,0 +1,68 @@
// 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 email
import (
"encoding/json"
"fmt"
"strings"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)
type SendgridEmailProvider struct {
ApiKey string
}
type SendgridResponseBody struct {
Errors []struct {
Message string `json:"message"`
Field interface{} `json:"field"`
Help interface{} `json:"help"`
} `json:"errors"`
}
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
return &SendgridEmailProvider{ApiKey: apiKey}
}
func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress string, subject string, content string) error {
from := mail.NewEmail(fromName, fromAddress)
to := mail.NewEmail("", toAddress)
message := mail.NewSingleEmail(from, subject, to, "", content)
client := sendgrid.NewSendClient(s.ApiKey)
response, err := client.Send(message)
if err != nil {
return err
}
if response.StatusCode >= 300 {
var responseBody SendgridResponseBody
err = json.Unmarshal([]byte(response.Body), &responseBody)
if err != nil {
return err
}
messages := []string{}
for _, sendgridError := range responseBody.Errors {
messages = append(messages, sendgridError.Message)
}
return fmt.Errorf("SendGrid status code: %d, error message: %s", response.StatusCode, strings.Join(messages, " | "))
}
return nil
}

5
go.mod
View File

@ -9,12 +9,12 @@ 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.20.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.6.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.3.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
github.com/denisenkom/go-mssqldb v0.9.0 github.com/denisenkom/go-mssqldb v0.9.0
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
@ -45,6 +45,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.9.0 github.com/russellhaering/gosaml2 v0.9.0
github.com/russellhaering/goxmldsig v1.2.0 github.com/russellhaering/goxmldsig v1.2.0
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible github.com/shirou/gopsutil v3.21.11+incompatible
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed

13
go.sum
View File

@ -1003,6 +1003,8 @@ github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0I
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg=
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/apistd/uni-go-sdk v0.0.2 h1:7kqETCOz/rz8AQU55XGzxDFGoFeMgeZL5fGwvxKBZrc=
github.com/apistd/uni-go-sdk v0.0.2/go.mod h1:eIqYos4IbHgE/rB75r05ypNLahooEMJCrbjXq322b74=
github.com/appleboy/go-fcm v0.1.5/go.mod h1:MSxZ4LqGRsnywOjnlXJXMqbjZrG4vf+0oHitfC9HRH0= github.com/appleboy/go-fcm v0.1.5/go.mod h1:MSxZ4LqGRsnywOjnlXJXMqbjZrG4vf+0oHitfC9HRH0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -1083,8 +1085,8 @@ github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3
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/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.20.0 h1:yLbCakV04DzzehhgBklOrSeCFjMwpfKBeemz9b+Y8OM= github.com/casdoor/go-sms-sender v0.24.0 h1:LNLsce3EG/87I3JS6UiajF3LlQmdIiCgebEu0IE4wSM=
github.com/casdoor/go-sms-sender v0.20.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs= 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=
@ -1093,8 +1095,8 @@ github.com/casdoor/oss v1.6.0 h1:IOWrGLJ+VO82qS796eaRnzFPPA1Sn3cotYTi7O/VIlQ=
github.com/casdoor/oss v1.6.0/go.mod h1:rJAWA0hLhtu94t6IRpotLUkXO1NWMASirywQYaGizJE= github.com/casdoor/oss v1.6.0/go.mod h1:rJAWA0hLhtu94t6IRpotLUkXO1NWMASirywQYaGizJE=
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.3.0 h1:HVgm2g3lWpNX2wBNidzR743QY4O5kAjLUJ9tS2juO8g= github.com/casvisor/casvisor-go-sdk v1.4.0 h1:hbZEGGJ1cwdHFAxeXrMoNw6yha6Oyg2F0qQhBNCN/dg=
github.com/casvisor/casvisor-go-sdk v1.3.0/go.mod h1:frnNtH5GA0wxzAQLyZxxfL0RSsSub9GQPi2Ybe86ocE= github.com/casvisor/casvisor-go-sdk v1.4.0/go.mod h1:frnNtH5GA0wxzAQLyZxxfL0RSsSub9GQPi2Ybe86ocE=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
@ -1907,8 +1909,11 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM= github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM=
github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA= github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "Der Benutzer %s existiert nicht", "The user: %s doesn't exist": "Der Benutzer %s existiert nicht",
"don't support captchaProvider: ": "Unterstütze captchaProvider nicht:", "don't support captchaProvider: ": "Unterstütze captchaProvider nicht:",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Es gibt einen LDAP-Server" "Ldap server exist": "Es gibt einen LDAP-Server"
@ -145,9 +146,10 @@
"The provider: %s is not found": "Der Anbieter: %s wurde nicht gefunden" "The provider: %s is not found": "Der Anbieter: %s wurde nicht gefunden"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Der Code wurde noch nicht versendet!",
"Invalid captcha provider.": "Ungültiger Captcha-Anbieter.", "Invalid captcha provider.": "Ungültiger Captcha-Anbieter.",
"Phone number is invalid in your region %s": "Die Telefonnummer ist in Ihrer Region %s ungültig", "Phone number is invalid in your region %s": "Die Telefonnummer ist in Ihrer Region %s ungültig",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing-Test fehlgeschlagen.", "Turing test failed.": "Turing-Test fehlgeschlagen.",
"Unable to get the email modify rule.": "Nicht in der Lage, die E-Mail-Änderungsregel zu erhalten.", "Unable to get the email modify rule.": "Nicht in der Lage, die E-Mail-Änderungsregel zu erhalten.",
"Unable to get the phone modify rule.": "Nicht in der Lage, die Telefon-Änderungsregel zu erhalten.", "Unable to get the phone modify rule.": "Nicht in der Lage, die Telefon-Änderungsregel zu erhalten.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "El usuario: %s no existe", "The user: %s doesn't exist": "El usuario: %s no existe",
"don't support captchaProvider: ": "No apoyo a captchaProvider", "don't support captchaProvider: ": "No apoyo a captchaProvider",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "El servidor LDAP existe" "Ldap server exist": "El servidor LDAP existe"
@ -145,9 +146,10 @@
"The provider: %s is not found": "El proveedor: %s no se encuentra" "The provider: %s is not found": "El proveedor: %s no se encuentra"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "¡El código aún no ha sido enviado!",
"Invalid captcha provider.": "Proveedor de captcha no válido.", "Invalid captcha provider.": "Proveedor de captcha no válido.",
"Phone number is invalid in your region %s": "El número de teléfono es inválido en tu región %s", "Phone number is invalid in your region %s": "El número de teléfono es inválido en tu región %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "El test de Turing falló.", "Turing test failed.": "El test de Turing falló.",
"Unable to get the email modify rule.": "No se puede obtener la regla de modificación de correo electrónico.", "Unable to get the email modify rule.": "No se puede obtener la regla de modificación de correo electrónico.",
"Unable to get the phone modify rule.": "No se pudo obtener la regla de modificación del teléfono.", "Unable to get the phone modify rule.": "No se pudo obtener la regla de modificación del teléfono.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "L'utilisateur : %s n'existe pas", "The user: %s doesn't exist": "L'utilisateur : %s n'existe pas",
"don't support captchaProvider: ": "ne prend pas en charge captchaProvider: ", "don't support captchaProvider: ": "ne prend pas en charge captchaProvider: ",
"this operation is not allowed in demo mode": "cette opération nest pas autorisée en mode démo" "this operation is not allowed in demo mode": "cette opération nest pas autorisée en mode démo",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Le serveur LDAP existe" "Ldap server exist": "Le serveur LDAP existe"
@ -145,9 +146,10 @@
"The provider: %s is not found": "Le fournisseur : %s n'a pas été trouvé" "The provider: %s is not found": "Le fournisseur : %s n'a pas été trouvé"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Le code n'a pas encore été envoyé !",
"Invalid captcha provider.": "Fournisseur de captcha invalide.", "Invalid captcha provider.": "Fournisseur de captcha invalide.",
"Phone number is invalid in your region %s": "Le numéro de téléphone n'est pas valide dans votre région %s", "Phone number is invalid in your region %s": "Le numéro de téléphone n'est pas valide dans votre région %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Le test de Turing a échoué.", "Turing test failed.": "Le test de Turing a échoué.",
"Unable to get the email modify rule.": "Incapable d'obtenir la règle de modification de courriel.", "Unable to get the email modify rule.": "Incapable d'obtenir la règle de modification de courriel.",
"Unable to get the phone modify rule.": "Impossible d'obtenir la règle de modification de téléphone.", "Unable to get the phone modify rule.": "Impossible d'obtenir la règle de modification de téléphone.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "Pengguna: %s tidak ada", "The user: %s doesn't exist": "Pengguna: %s tidak ada",
"don't support captchaProvider: ": "Jangan mendukung captchaProvider:", "don't support captchaProvider: ": "Jangan mendukung captchaProvider:",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Server ldap ada" "Ldap server exist": "Server ldap ada"
@ -145,9 +146,10 @@
"The provider: %s is not found": "Penyedia: %s tidak ditemukan" "The provider: %s is not found": "Penyedia: %s tidak ditemukan"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Kode belum dikirimkan!",
"Invalid captcha provider.": "Penyedia captcha tidak valid.", "Invalid captcha provider.": "Penyedia captcha tidak valid.",
"Phone number is invalid in your region %s": "Nomor telepon tidak valid di wilayah anda %s", "Phone number is invalid in your region %s": "Nomor telepon tidak valid di wilayah anda %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Tes Turing gagal.", "Turing test failed.": "Tes Turing gagal.",
"Unable to get the email modify rule.": "Tidak dapat memperoleh aturan modifikasi email.", "Unable to get the email modify rule.": "Tidak dapat memperoleh aturan modifikasi email.",
"Unable to get the phone modify rule.": "Tidak dapat memodifikasi aturan telepon.", "Unable to get the phone modify rule.": "Tidak dapat memodifikasi aturan telepon.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "そのユーザー:%sは存在しません", "The user: %s doesn't exist": "そのユーザー:%sは存在しません",
"don't support captchaProvider: ": "captchaProviderをサポートしないでください", "don't support captchaProvider: ": "captchaProviderをサポートしないでください",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "LDAPサーバーは存在します" "Ldap server exist": "LDAPサーバーは存在します"
@ -145,9 +146,10 @@
"The provider: %s is not found": "プロバイダー:%sが見つかりません" "The provider: %s is not found": "プロバイダー:%sが見つかりません"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "まだコードが送信されていません!",
"Invalid captcha provider.": "無効なCAPTCHAプロバイダー。", "Invalid captcha provider.": "無効なCAPTCHAプロバイダー。",
"Phone number is invalid in your region %s": "電話番号はあなたの地域で無効です %s", "Phone number is invalid in your region %s": "電話番号はあなたの地域で無効です %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "チューリングテストは失敗しました。", "Turing test failed.": "チューリングテストは失敗しました。",
"Unable to get the email modify rule.": "電子メール変更規則を取得できません。", "Unable to get the email modify rule.": "電子メール変更規則を取得できません。",
"Unable to get the phone modify rule.": "電話の変更ルールを取得できません。", "Unable to get the phone modify rule.": "電話の変更ルールを取得できません。",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "사용자 %s는 존재하지 않습니다", "The user: %s doesn't exist": "사용자 %s는 존재하지 않습니다",
"don't support captchaProvider: ": "CaptchaProvider를 지원하지 마세요", "don't support captchaProvider: ": "CaptchaProvider를 지원하지 마세요",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "LDAP 서버가 존재합니다" "Ldap server exist": "LDAP 서버가 존재합니다"
@ -145,9 +146,10 @@
"The provider: %s is not found": "제공자: %s를 찾을 수 없습니다" "The provider: %s is not found": "제공자: %s를 찾을 수 없습니다"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "코드는 아직 전송되지 않았습니다!",
"Invalid captcha provider.": "잘못된 captcha 제공자입니다.", "Invalid captcha provider.": "잘못된 captcha 제공자입니다.",
"Phone number is invalid in your region %s": "전화 번호가 당신의 지역 %s에서 유효하지 않습니다", "Phone number is invalid in your region %s": "전화 번호가 당신의 지역 %s에서 유효하지 않습니다",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "튜링 테스트 실패.", "Turing test failed.": "튜링 테스트 실패.",
"Unable to get the email modify rule.": "이메일 수정 규칙을 가져올 수 없습니다.", "Unable to get the email modify rule.": "이메일 수정 규칙을 가져올 수 없습니다.",
"Unable to get the phone modify rule.": "전화 수정 규칙을 가져올 수 없습니다.", "Unable to get the phone modify rule.": "전화 수정 규칙을 가져올 수 없습니다.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "Пользователь %s не существует", "The user: %s doesn't exist": "Пользователь %s не существует",
"don't support captchaProvider: ": "неподдерживаемый captchaProvider: ", "don't support captchaProvider: ": "неподдерживаемый captchaProvider: ",
"this operation is not allowed in demo mode": "эта операция не разрешена в демо-режиме" "this operation is not allowed in demo mode": "эта операция не разрешена в демо-режиме",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "LDAP-сервер существует" "Ldap server exist": "LDAP-сервер существует"
@ -145,9 +146,10 @@
"The provider: %s is not found": "Поставщик: %s не найден" "The provider: %s is not found": "Поставщик: %s не найден"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Код еще не был отправлен!",
"Invalid captcha provider.": "Недействительный поставщик CAPTCHA.", "Invalid captcha provider.": "Недействительный поставщик CAPTCHA.",
"Phone number is invalid in your region %s": "Номер телефона недействителен в вашем регионе %s", "Phone number is invalid in your region %s": "Номер телефона недействителен в вашем регионе %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Тест Тьюринга не удался.", "Turing test failed.": "Тест Тьюринга не удался.",
"Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.", "Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.",
"Unable to get the phone modify rule.": "Невозможно получить правило изменения телефона.", "Unable to get the phone modify rule.": "Невозможно получить правило изменения телефона.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Telefon numaranızın bulunduğu bölgeye hizmet veremiyoruz", "Phone number is invalid in your region %s": "Telefon numaranızın bulunduğu bölgeye hizmet veremiyoruz",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "The user: %s doesn't exist", "The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ", "don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Ldap server exist" "Ldap server exist": "Ldap server exist"
@ -145,9 +146,10 @@
"The provider: %s is not found": "The provider: %s is not found" "The provider: %s is not found": "The provider: %s is not found"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.", "Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s", "Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Turing test failed.", "Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.", "Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.", "Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The user: %s doesn't exist": "Người dùng: %s không tồn tại", "The user: %s doesn't exist": "Người dùng: %s không tồn tại",
"don't support captchaProvider: ": "không hỗ trợ captchaProvider: ", "don't support captchaProvider: ": "không hỗ trợ captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode" "this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Máy chủ LDAP tồn tại" "Ldap server exist": "Máy chủ LDAP tồn tại"
@ -145,9 +146,10 @@
"The provider: %s is not found": "Nhà cung cấp: %s không được tìm thấy" "The provider: %s is not found": "Nhà cung cấp: %s không được tìm thấy"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "Mã chưa được gửi đến!",
"Invalid captcha provider.": "Nhà cung cấp captcha không hợp lệ.", "Invalid captcha provider.": "Nhà cung cấp captcha không hợp lệ.",
"Phone number is invalid in your region %s": "Số điện thoại không hợp lệ trong vùng của bạn %s", "Phone number is invalid in your region %s": "Số điện thoại không hợp lệ trong vùng của bạn %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
"Turing test failed.": "Kiểm định Turing thất bại.", "Turing test failed.": "Kiểm định Turing thất bại.",
"Unable to get the email modify rule.": "Không thể lấy quy tắc sửa đổi email.", "Unable to get the email modify rule.": "Không thể lấy quy tắc sửa đổi email.",
"Unable to get the phone modify rule.": "Không thể thay đổi quy tắc trên điện thoại.", "Unable to get the phone modify rule.": "Không thể thay đổi quy tắc trên điện thoại.",

View File

@ -81,7 +81,8 @@
"The organization: %s should have one application at least": "组织: %s 应该拥有至少一个应用", "The organization: %s should have one application at least": "组织: %s 应该拥有至少一个应用",
"The user: %s doesn't exist": "用户: %s不存在", "The user: %s doesn't exist": "用户: %s不存在",
"don't support captchaProvider: ": "不支持验证码提供商: ", "don't support captchaProvider: ": "不支持验证码提供商: ",
"this operation is not allowed in demo mode": "demo模式下不允许该操作" "this operation is not allowed in demo mode": "demo模式下不允许该操作",
"this operation requires administrator to perform": "只有管理员才能进行此操作"
}, },
"ldap": { "ldap": {
"Ldap server exist": "LDAP服务器已存在" "Ldap server exist": "LDAP服务器已存在"
@ -145,9 +146,10 @@
"The provider: %s is not found": "未找到提供商: %s" "The provider: %s is not found": "未找到提供商: %s"
}, },
"verification": { "verification": {
"Code has not been sent yet!": "验证码还未发送",
"Invalid captcha provider.": "非法的验证码提供商", "Invalid captcha provider.": "非法的验证码提供商",
"Phone number is invalid in your region %s": "您所在地区的电话号码无效 %s", "Phone number is invalid in your region %s": "您所在地区的电话号码无效 %s",
"The verification code has not been sent yet!": "验证码未发送!",
"The verification code has not been sent yet, or has already been used!": "验证码未发送或已被使用!",
"Turing test failed.": "验证码还未发送", "Turing test failed.": "验证码还未发送",
"Unable to get the email modify rule.": "无法获取邮箱修改规则", "Unable to get the email modify rule.": "无法获取邮箱修改规则",
"Unable to get the phone modify rule.": "无法获取手机号修改规则", "Unable to get the phone modify rule.": "无法获取手机号修改规则",

View File

@ -25,6 +25,7 @@ import (
"time" "time"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/nyaruka/phonenumbers"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -130,6 +131,23 @@ type GoogleUserInfo struct {
Locale string `json:"locale"` Locale string `json:"locale"`
} }
type GooglePeopleApiPhoneNumberMetaData struct {
Primary bool `json:"primary"`
}
type GooglePeopleApiPhoneNumber struct {
CanonicalForm string `json:"canonicalForm"`
MetaData GooglePeopleApiPhoneNumberMetaData `json:"metadata"`
Value string `json:"value"`
Type string `json:"type"`
}
type GooglePeopleApiResult struct {
PhoneNumbers []GooglePeopleApiPhoneNumber `json:"phoneNumbers"`
Etag string `json:"etag"`
ResourceName string `json:"resourceName"`
}
func (idp *GoogleIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { func (idp *GoogleIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
if strings.HasPrefix(token.AccessToken, GoogleIdTokenKey) { if strings.HasPrefix(token.AccessToken, GoogleIdTokenKey) {
googleIdToken, ok := token.Extra(GoogleIdTokenKey).(GoogleIdToken) googleIdToken, ok := token.Extra(GoogleIdTokenKey).(GoogleIdToken)
@ -167,12 +185,49 @@ func (idp *GoogleIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
return nil, errors.New("google email is empty") return nil, errors.New("google email is empty")
} }
url = fmt.Sprintf("https://people.googleapis.com/v1/people/me?personFields=phoneNumbers&access_token=%s", token.AccessToken)
resp, err = idp.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var googlePeopleResult GooglePeopleApiResult
err = json.Unmarshal(body, &googlePeopleResult)
if err != nil {
return nil, err
}
var phoneNumber string
var countryCode string
if len(googlePeopleResult.PhoneNumbers) != 0 {
for _, phoneData := range googlePeopleResult.PhoneNumbers {
if phoneData.MetaData.Primary {
phoneNumber = phoneData.CanonicalForm
break
}
}
phoneNumberParsed, err := phonenumbers.Parse(phoneNumber, "")
if err != nil {
return nil, err
}
countryCode = phonenumbers.GetRegionCodeForNumber(phoneNumberParsed)
phoneNumber = fmt.Sprintf("%d", phoneNumberParsed.GetNationalNumber())
}
userInfo := UserInfo{ userInfo := UserInfo{
Id: googleUserInfo.Id, Id: googleUserInfo.Id,
Username: googleUserInfo.Email, Username: googleUserInfo.Email,
DisplayName: googleUserInfo.Name, DisplayName: googleUserInfo.Name,
Email: googleUserInfo.Email, Email: googleUserInfo.Email,
AvatarUrl: googleUserInfo.Picture, AvatarUrl: googleUserInfo.Picture,
Phone: phoneNumber,
CountryCode: countryCode,
} }
return &userInfo, nil return &userInfo, nil
} }

View File

@ -35,6 +35,7 @@ type SignupItem struct {
Visible bool `json:"visible"` Visible bool `json:"visible"`
Required bool `json:"required"` Required bool `json:"required"`
Prompted bool `json:"prompted"` Prompted bool `json:"prompted"`
CustomCss string `json:"customCss"`
Label string `json:"label"` Label string `json:"label"`
Placeholder string `json:"placeholder"` Placeholder string `json:"placeholder"`
Regex string `json:"regex"` Regex string `json:"regex"`
@ -45,6 +46,7 @@ type SigninItem struct {
Name string `json:"name"` Name string `json:"name"`
Visible bool `json:"visible"` Visible bool `json:"visible"`
Label string `json:"label"` Label string `json:"label"`
CustomCss string `json:"customCss"`
Placeholder string `json:"placeholder"` Placeholder string `json:"placeholder"`
Rule string `json:"rule"` Rule string `json:"rule"`
IsCustom bool `json:"isCustom"` IsCustom bool `json:"isCustom"`
@ -208,7 +210,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem := &SigninItem{ signinItem := &SigninItem{
Name: "Back button", Name: "Back button",
Visible: true, Visible: true,
Label: "\n<style>\n .back-button {\n top: 65px;\n left: 15px;\n position: absolute;\n }\n</style>\n", CustomCss: ".back-button {\n top: 65px;\n left: 15px;\n position: absolute;\n}\n.back-inner-button{}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -216,7 +218,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Languages", Name: "Languages",
Visible: true, Visible: true,
Label: "\n<style>\n .login-languages {\n top: 55px;\n right: 5px;\n position: absolute;\n }\n</style>\n", CustomCss: ".login-languages {\n top: 55px;\n right: 5px;\n position: absolute;\n}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -224,7 +226,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Logo", Name: "Logo",
Visible: true, Visible: true,
Label: "\n<style>\n .login-logo-box {\n }\n</style>\n", CustomCss: ".login-logo-box {}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -232,7 +234,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Signin methods", Name: "Signin methods",
Visible: true, Visible: true,
Label: "\n<style>\n .signin-methods {\n }\n</style>\n", CustomCss: ".signin-methods {}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -240,7 +242,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Username", Name: "Username",
Visible: true, Visible: true,
Label: "\n<style>\n .login-username {\n }\n</style>\n", CustomCss: ".login-username {}\n.login-username-input{}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -248,7 +250,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Password", Name: "Password",
Visible: true, Visible: true,
Label: "\n<style>\n .login-password {\n }\n</style>\n", CustomCss: ".login-password {}\n.login-password-input{}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -256,7 +258,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Agreement", Name: "Agreement",
Visible: true, Visible: true,
Label: "\n<style>\n .login-agreement {\n }\n</style>\n", CustomCss: ".login-agreement {}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -264,7 +266,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Forgot password?", Name: "Forgot password?",
Visible: true, Visible: true,
Label: "\n<style>\n .login-forget-password {\n display: inline-flex;\n justify-content: space-between;\n width: 320px;\n margin-bottom: 25px;\n }\n</style>\n", CustomCss: ".login-forget-password {\n display: inline-flex;\n justify-content: space-between;\n width: 320px;\n margin-bottom: 25px;\n}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -272,7 +274,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Login button", Name: "Login button",
Visible: true, Visible: true,
Label: "\n<style>\n .login-button-box {\n margin-bottom: 5px;\n }\n .login-button {\n width: 100%;\n }\n</style>\n", CustomCss: ".login-button-box {\n margin-bottom: 5px;\n}\n.login-button {\n width: 100%;\n}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -280,7 +282,7 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Signup link", Name: "Signup link",
Visible: true, Visible: true,
Label: "\n<style>\n .login-signup-link {\n margin-bottom: 24px;\n display: flex;\n justify-content: end;\n}\n</style>\n", CustomCss: ".login-signup-link {\n margin-bottom: 24px;\n display: flex;\n justify-content: end;\n}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
@ -288,12 +290,18 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
signinItem = &SigninItem{ signinItem = &SigninItem{
Name: "Providers", Name: "Providers",
Visible: true, Visible: true,
Label: "\n<style>\n .provider-img {\n width: 30px;\n margin: 5px;\n }\n .provider-big-img {\n margin-bottom: 10px;\n }\n</style>\n", CustomCss: ".provider-img {\n width: 30px;\n margin: 5px;\n}\n.provider-big-img {\n margin-bottom: 10px;\n}",
Placeholder: "", Placeholder: "",
Rule: "None", Rule: "None",
} }
application.SigninItems = append(application.SigninItems, signinItem) application.SigninItems = append(application.SigninItems, signinItem)
} }
for idx, item := range application.SigninItems {
if item.Label != "" && item.CustomCss == "" {
application.SigninItems[idx].CustomCss = item.Label
application.SigninItems[idx].Label = ""
}
}
return return
} }
@ -404,8 +412,8 @@ func GetApplicationByUser(user *User) (*Application, error) {
} }
func GetApplicationByUserId(userId string) (application *Application, err error) { func GetApplicationByUserId(userId string) (application *Application, err error) {
owner, name := util.GetOwnerAndNameFromId(userId) _, name := util.GetOwnerAndNameFromId(userId)
if owner == "app" { if IsAppUser(userId) {
application, err = getApplication("admin", name) application, err = getApplication("admin", name)
return return
} }
@ -669,11 +677,7 @@ func AddApplication(application *Application) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func DeleteApplication(application *Application) (bool, error) { func deleteApplication(application *Application) (bool, error) {
if application.Name == "app-built-in" {
return false, nil
}
affected, err := ormer.Engine.ID(core.PK{application.Owner, application.Name}).Delete(&Application{}) affected, err := ormer.Engine.ID(core.PK{application.Owner, application.Name}).Delete(&Application{})
if err != nil { if err != nil {
return false, err return false, err
@ -682,6 +686,14 @@ func DeleteApplication(application *Application) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func DeleteApplication(application *Application) (bool, error) {
if application.Name == "app-built-in" {
return false, nil
}
return deleteApplication(application)
}
func (application *Application) GetId() string { func (application *Application) GetId() string {
return fmt.Sprintf("%s/%s", application.Owner, application.Name) return fmt.Sprintf("%s/%s", application.Owner, application.Name)
} }

View File

@ -38,7 +38,20 @@ func (application *Application) GetProviderByCategory(category string) (*Provide
return nil, nil return nil, nil
} }
func (application *Application) GetProviderByCategoryAndRule(category string, method string) (*Provider, error) { func isProviderItemCountryCodeMatched(providerItem *ProviderItem, countryCode string) bool {
if len(providerItem.CountryCodes) == 0 {
return true
}
for _, countryCode2 := range providerItem.CountryCodes {
if countryCode2 == "" || countryCode2 == "All" || countryCode2 == "all" || countryCode2 == countryCode {
return true
}
}
return false
}
func (application *Application) GetProviderByCategoryAndRule(category string, method string, countryCode string) (*Provider, error) {
providers, err := GetProviders(application.Organization) providers, err := GetProviders(application.Organization)
if err != nil { if err != nil {
return nil, err return nil, err
@ -54,7 +67,13 @@ func (application *Application) GetProviderByCategoryAndRule(category string, me
} }
for _, providerItem := range application.Providers { for _, providerItem := range application.Providers {
if providerItem.Rule == method || (providerItem.Rule == "all" || providerItem.Rule == "" || providerItem.Rule == "None") { if providerItem.Provider != nil && providerItem.Provider.Category == "SMS" {
if !isProviderItemCountryCodeMatched(providerItem, countryCode) {
continue
}
}
if providerItem.Rule == method || providerItem.Rule == "" || providerItem.Rule == "All" || providerItem.Rule == "all" || providerItem.Rule == "None" {
if provider, ok := m[providerItem.Name]; ok { if provider, ok := m[providerItem.Name]; ok {
return provider, nil return provider, nil
} }
@ -65,11 +84,11 @@ func (application *Application) GetProviderByCategoryAndRule(category string, me
} }
func (application *Application) GetEmailProvider(method string) (*Provider, error) { func (application *Application) GetEmailProvider(method string) (*Provider, error) {
return application.GetProviderByCategoryAndRule("Email", method) return application.GetProviderByCategoryAndRule("Email", method, "All")
} }
func (application *Application) GetSmsProvider(method string) (*Provider, error) { func (application *Application) GetSmsProvider(method string, countryCode string) (*Provider, error) {
return application.GetProviderByCategoryAndRule("SMS", method) return application.GetProviderByCategoryAndRule("SMS", method, countryCode)
} }
func (application *Application) GetStorageProvider() (*Provider, error) { func (application *Application) GetStorageProvider() (*Provider, error) {

View File

@ -410,7 +410,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
} }
hasPermission := false hasPermission := false
if strings.HasPrefix(requestUserId, "app/") { if IsAppUser(requestUserId) {
hasPermission = true hasPermission = true
} else { } else {
requestUser, err := GetUser(requestUserId) requestUser, err := GetUser(requestUserId)

View File

@ -154,6 +154,15 @@ func AddGroups(groups []*Group) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func deleteGroup(group *Group) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteGroup(group *Group) (bool, error) { func DeleteGroup(group *Group) (bool, error) {
_, err := ormer.Engine.Get(group) _, err := ormer.Engine.Get(group)
if err != nil { if err != nil {
@ -172,12 +181,7 @@ func DeleteGroup(group *Group) (bool, error) {
return false, errors.New("group has users") return false, errors.New("group has users")
} }
affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{}) return deleteGroup(group)
if err != nil {
return false, err
}
return affected != 0, nil
} }
func checkGroupName(name string) error { func checkGroupName(name string) error {

View File

@ -71,7 +71,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"}, {Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Properties", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
@ -108,6 +108,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 {
@ -409,7 +411,7 @@ func initBuiltInPermission() {
Groups: []string{}, Groups: []string{},
Roles: []string{}, Roles: []string{},
Domains: []string{}, Domains: []string{},
Model: "model-built-in", Model: "user-model-built-in",
Adapter: "", Adapter: "",
ResourceType: "Application", ResourceType: "Application",
Resources: []string{"app-built-in"}, Resources: []string{"app-built-in"},

View File

@ -266,7 +266,13 @@ func initDefinedOrganization(organization *Organization) {
} }
if existed != nil { if existed != nil {
return affected, err := deleteOrganization(organization)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete organization")
}
} }
organization.CreatedTime = util.GetCurrentTime() organization.CreatedTime = util.GetCurrentTime()
organization.AccountItems = getBuiltInAccountItems() organization.AccountItems = getBuiltInAccountItems()
@ -284,7 +290,13 @@ func initDefinedApplication(application *Application) {
} }
if existed != nil { if existed != nil {
return affected, err := deleteApplication(application)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete application")
}
} }
application.CreatedTime = util.GetCurrentTime() application.CreatedTime = util.GetCurrentTime()
_, err = AddApplication(application) _, err = AddApplication(application)
@ -299,11 +311,19 @@ func initDefinedUser(user *User) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := deleteUser(user)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete user")
}
} }
user.CreatedTime = util.GetCurrentTime() user.CreatedTime = util.GetCurrentTime()
user.Id = util.GenerateId() user.Id = util.GenerateId()
user.Properties = make(map[string]string) if user.Properties == nil {
user.Properties = make(map[string]string)
}
_, err = AddUser(user) _, err = AddUser(user)
if err != nil { if err != nil {
panic(err) panic(err)
@ -317,7 +337,13 @@ func initDefinedCert(cert *Cert) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteCert(cert)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete cert")
}
} }
cert.CreatedTime = util.GetCurrentTime() cert.CreatedTime = util.GetCurrentTime()
_, err = AddCert(cert) _, err = AddCert(cert)
@ -333,7 +359,13 @@ func initDefinedLdap(ldap *Ldap) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteLdap(ldap)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete ldap")
}
} }
_, err = AddLdap(ldap) _, err = AddLdap(ldap)
if err != nil { if err != nil {
@ -348,7 +380,13 @@ func initDefinedProvider(provider *Provider) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteProvider(provider)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete provider")
}
} }
_, err = AddProvider(provider) _, err = AddProvider(provider)
if err != nil { if err != nil {
@ -363,7 +401,13 @@ func initDefinedModel(model *Model) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteModel(model)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete provider")
}
} }
model.CreatedTime = util.GetCurrentTime() model.CreatedTime = util.GetCurrentTime()
_, err = AddModel(model) _, err = AddModel(model)
@ -379,7 +423,13 @@ func initDefinedPermission(permission *Permission) {
} }
if existed != nil { if existed != nil {
return affected, err := deletePermission(permission)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete permission")
}
} }
permission.CreatedTime = util.GetCurrentTime() permission.CreatedTime = util.GetCurrentTime()
_, err = AddPermission(permission) _, err = AddPermission(permission)
@ -395,7 +445,13 @@ func initDefinedPayment(payment *Payment) {
} }
if existed != nil { if existed != nil {
return affected, err := DeletePayment(payment)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete payment")
}
} }
payment.CreatedTime = util.GetCurrentTime() payment.CreatedTime = util.GetCurrentTime()
_, err = AddPayment(payment) _, err = AddPayment(payment)
@ -411,7 +467,13 @@ func initDefinedProduct(product *Product) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteProduct(product)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete product")
}
} }
product.CreatedTime = util.GetCurrentTime() product.CreatedTime = util.GetCurrentTime()
_, err = AddProduct(product) _, err = AddProduct(product)
@ -427,7 +489,13 @@ func initDefinedResource(resource *Resource) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteResource(resource)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete resource")
}
} }
resource.CreatedTime = util.GetCurrentTime() resource.CreatedTime = util.GetCurrentTime()
_, err = AddResource(resource) _, err = AddResource(resource)
@ -443,7 +511,13 @@ func initDefinedRole(role *Role) {
} }
if existed != nil { if existed != nil {
return affected, err := deleteRole(role)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete role")
}
} }
role.CreatedTime = util.GetCurrentTime() role.CreatedTime = util.GetCurrentTime()
_, err = AddRole(role) _, err = AddRole(role)
@ -459,7 +533,13 @@ func initDefinedSyncer(syncer *Syncer) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteSyncer(syncer)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete role")
}
} }
syncer.CreatedTime = util.GetCurrentTime() syncer.CreatedTime = util.GetCurrentTime()
_, err = AddSyncer(syncer) _, err = AddSyncer(syncer)
@ -475,7 +555,13 @@ func initDefinedToken(token *Token) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteToken(token)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete token")
}
} }
token.CreatedTime = util.GetCurrentTime() token.CreatedTime = util.GetCurrentTime()
_, err = AddToken(token) _, err = AddToken(token)
@ -491,7 +577,13 @@ func initDefinedWebhook(webhook *Webhook) {
} }
if existed != nil { if existed != nil {
return affected, err := DeleteWebhook(webhook)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete webhook")
}
} }
webhook.CreatedTime = util.GetCurrentTime() webhook.CreatedTime = util.GetCurrentTime()
_, err = AddWebhook(webhook) _, err = AddWebhook(webhook)
@ -506,7 +598,13 @@ func initDefinedGroup(group *Group) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := deleteGroup(group)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete group")
}
} }
group.CreatedTime = util.GetCurrentTime() group.CreatedTime = util.GetCurrentTime()
_, err = AddGroup(group) _, err = AddGroup(group)
@ -521,7 +619,13 @@ func initDefinedAdapter(adapter *Adapter) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := DeleteAdapter(adapter)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete adapter")
}
} }
adapter.CreatedTime = util.GetCurrentTime() adapter.CreatedTime = util.GetCurrentTime()
_, err = AddAdapter(adapter) _, err = AddAdapter(adapter)
@ -536,7 +640,13 @@ func initDefinedEnforcer(enforcer *Enforcer) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := DeleteEnforcer(enforcer)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete enforcer")
}
} }
enforcer.CreatedTime = util.GetCurrentTime() enforcer.CreatedTime = util.GetCurrentTime()
_, err = AddEnforcer(enforcer) _, err = AddEnforcer(enforcer)
@ -551,7 +661,13 @@ func initDefinedPlan(plan *Plan) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := DeletePlan(plan)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete plan")
}
} }
plan.CreatedTime = util.GetCurrentTime() plan.CreatedTime = util.GetCurrentTime()
_, err = AddPlan(plan) _, err = AddPlan(plan)
@ -561,12 +677,18 @@ func initDefinedPlan(plan *Plan) {
} }
func initDefinedPricing(pricing *Pricing) { func initDefinedPricing(pricing *Pricing) {
existed, err := getPlan(pricing.Owner, pricing.Name) existed, err := getPricing(pricing.Owner, pricing.Name)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := DeletePricing(pricing)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete pricing")
}
} }
pricing.CreatedTime = util.GetCurrentTime() pricing.CreatedTime = util.GetCurrentTime()
_, err = AddPricing(pricing) _, err = AddPricing(pricing)
@ -581,7 +703,13 @@ func initDefinedInvitation(invitation *Invitation) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := DeleteInvitation(invitation)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete invitation")
}
} }
invitation.CreatedTime = util.GetCurrentTime() invitation.CreatedTime = util.GetCurrentTime()
_, err = AddInvitation(invitation, "en") _, err = AddInvitation(invitation, "en")
@ -591,6 +719,7 @@ func initDefinedInvitation(invitation *Invitation) {
} }
func initDefinedRecord(record *casvisorsdk.Record) { func initDefinedRecord(record *casvisorsdk.Record) {
record.Id = 0
record.CreatedTime = util.GetCurrentTime() record.CreatedTime = util.GetCurrentTime()
_ = AddRecord(record) _ = AddRecord(record)
} }
@ -609,7 +738,13 @@ func initDefinedSubscription(subscription *Subscription) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := DeleteSubscription(subscription)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete subscription")
}
} }
subscription.CreatedTime = util.GetCurrentTime() subscription.CreatedTime = util.GetCurrentTime()
_, err = AddSubscription(subscription) _, err = AddSubscription(subscription)
@ -624,7 +759,13 @@ func initDefinedTransaction(transaction *Transaction) {
panic(err) panic(err)
} }
if existed != nil { if existed != nil {
return affected, err := DeleteTransaction(transaction)
if err != nil {
panic(err)
}
if !affected {
panic("Fail to delete transaction")
}
} }
transaction.CreatedTime = util.GetCurrentTime() transaction.CreatedTime = util.GetCurrentTime()
_, err = AddTransaction(transaction) _, err = AddTransaction(transaction)

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"`
@ -241,11 +243,7 @@ func AddOrganization(organization *Organization) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func DeleteOrganization(organization *Organization) (bool, error) { func deleteOrganization(organization *Organization) (bool, error) {
if organization.Name == "built-in" {
return false, nil
}
affected, err := ormer.Engine.ID(core.PK{organization.Owner, organization.Name}).Delete(&Organization{}) affected, err := ormer.Engine.ID(core.PK{organization.Owner, organization.Name}).Delete(&Organization{})
if err != nil { if err != nil {
return false, err return false, err
@ -254,6 +252,14 @@ func DeleteOrganization(organization *Organization) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func DeleteOrganization(organization *Organization) (bool, error) {
if organization.Name == "built-in" {
return false, nil
}
return deleteOrganization(organization)
}
func GetOrganizationByUser(user *User) (*Organization, error) { func GetOrganizationByUser(user *User) (*Organization, error) {
if user == nil { if user == nil {
return nil, nil return nil, 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
} }

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 {
@ -286,13 +286,22 @@ func AddPermissionsInBatch(permissions []*Permission) (bool, error) {
return affected, nil return affected, nil
} }
func DeletePermission(permission *Permission) (bool, error) { func deletePermission(permission *Permission) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{permission.Owner, permission.Name}).Delete(&Permission{}) affected, err := ormer.Engine.ID(core.PK{permission.Owner, permission.Name}).Delete(&Permission{})
if err != nil { if err != nil {
return false, err return false, err
} }
if affected != 0 { return affected != 0, nil
}
func DeletePermission(permission *Permission) (bool, error) {
affected, err := deletePermission(permission)
if err != nil {
return false, err
}
if affected {
err = removeGroupingPolicies(permission) err = removeGroupingPolicies(permission)
if err != nil { if err != nil {
return false, err return false, err
@ -303,18 +312,18 @@ 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 != 0, nil return affected, nil
} }
func getPermissionsByUser(userId string) ([]*Permission, error) { func getPermissionsByUser(userId string) ([]*Permission, error) {

View File

@ -133,7 +133,7 @@ func AddPlan(plan *Plan) (bool, error) {
} }
func DeletePlan(plan *Plan) (bool, error) { func DeletePlan(plan *Plan) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{plan.Owner, plan.Name}).Delete(plan) affected, err := ormer.Engine.ID(core.PK{plan.Owner, plan.Name}).Delete(Plan{})
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@ -140,7 +140,7 @@ func AddPricing(pricing *Pricing) (bool, error) {
} }
func DeletePricing(pricing *Pricing) (bool, error) { func DeletePricing(pricing *Pricing) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{pricing.Owner, pricing.Name}).Delete(pricing) affected, err := ormer.Engine.ID(core.PK{pricing.Owner, pricing.Name}).Delete(Pricing{})
if err != nil { if err != nil {
return false, err return false, err
} }

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
@ -246,6 +255,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,
@ -256,6 +266,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
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
}
} }
affected, err := AddPayment(payment) affected, err := AddPayment(payment)
@ -304,8 +318,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

@ -50,7 +50,7 @@ type Provider struct {
Host string `xorm:"varchar(100)" json:"host"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `json:"port"`
DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode, if type is Google, it means sync phone number
Title string `xorm:"varchar(100)" json:"title"` Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"varchar(2000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding Content string `xorm:"varchar(2000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding
Receiver string `xorm:"varchar(100)" json:"receiver"` Receiver string `xorm:"varchar(100)" json:"receiver"`

View File

@ -18,13 +18,14 @@ type ProviderItem struct {
Owner string `json:"owner"` Owner string `json:"owner"`
Name string `json:"name"` Name string `json:"name"`
CanSignUp bool `json:"canSignUp"` CanSignUp bool `json:"canSignUp"`
CanSignIn bool `json:"canSignIn"` CanSignIn bool `json:"canSignIn"`
CanUnlink bool `json:"canUnlink"` CanUnlink bool `json:"canUnlink"`
Prompted bool `json:"prompted"` CountryCodes []string `json:"countryCodes"`
SignupGroup string `json:"signupGroup"` Prompted bool `json:"prompted"`
Rule string `json:"rule"` SignupGroup string `json:"signupGroup"`
Provider *Provider `json:"provider"` Rule string `json:"rule"`
Provider *Provider `json:"provider"`
} }
func (application *Application) GetProviderItem(providerName string) *ProviderItem { func (application *Application) GetProviderItem(providerName string) *ProviderItem {

View File

@ -17,6 +17,7 @@ package object
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
@ -25,10 +26,14 @@ import (
"github.com/casvisor/casvisor-go-sdk/casvisorsdk" "github.com/casvisor/casvisor-go-sdk/casvisorsdk"
) )
var logPostOnly bool var (
logPostOnly bool
passwordRegex *regexp.Regexp
)
func init() { func init() {
logPostOnly = conf.GetConfigBool("logPostOnly") logPostOnly = conf.GetConfigBool("logPostOnly")
passwordRegex = regexp.MustCompile("\"password\":\".+\"")
} }
type Record struct { type Record struct {
@ -40,6 +45,10 @@ type Response struct {
Msg string `json:"msg"` Msg string `json:"msg"`
} }
func maskPassword(recordString string) string {
return passwordRegex.ReplaceAllString(recordString, "\"password\":\"***\"")
}
func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) { func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
ip := strings.Replace(util.GetIPFromRequest(ctx.Request), ": ", "", -1) ip := strings.Replace(util.GetIPFromRequest(ctx.Request), ": ", "", -1)
action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1) action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1)
@ -51,6 +60,7 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
object := "" object := ""
if ctx.Input.RequestBody != nil && len(ctx.Input.RequestBody) != 0 { if ctx.Input.RequestBody != nil && len(ctx.Input.RequestBody) != 0 {
object = string(ctx.Input.RequestBody) object = string(ctx.Input.RequestBody)
object = maskPassword(object)
} }
respBytes, err := json.Marshal(ctx.Input.Data()["json"]) respBytes, err := json.Marshal(ctx.Input.Data()["json"])
@ -80,12 +90,18 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
Action: action, Action: action,
Language: languageCode, Language: languageCode,
Object: object, Object: object,
StatusCode: 200,
Response: fmt.Sprintf("{status:\"%s\", msg:\"%s\"}", resp.Status, resp.Msg), Response: fmt.Sprintf("{status:\"%s\", msg:\"%s\"}", resp.Status, resp.Msg),
IsTriggered: false, IsTriggered: false,
} }
return &record, nil return &record, nil
} }
func addRecord(record *casvisorsdk.Record) (int64, error) {
affected, err := ormer.Engine.Insert(record)
return affected, err
}
func AddRecord(record *casvisorsdk.Record) bool { func AddRecord(record *casvisorsdk.Record) bool {
if logPostOnly { if logPostOnly {
if record.Method == "GET" { if record.Method == "GET" {
@ -98,6 +114,7 @@ func AddRecord(record *casvisorsdk.Record) bool {
} }
record.Owner = record.Organization record.Owner = record.Organization
record.Object = maskPassword(record.Object)
errWebhook := SendWebhooks(record) errWebhook := SendWebhooks(record)
if errWebhook == nil { if errWebhook == nil {
@ -107,7 +124,7 @@ func AddRecord(record *casvisorsdk.Record) bool {
} }
if casvisorsdk.GetClient() == nil { if casvisorsdk.GetClient() == nil {
affected, err := ormer.Engine.Insert(record) affected, err := addRecord(record)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -212,6 +229,40 @@ func getFilteredWebhooks(webhooks []*Webhook, organization string, action string
return res return res
} }
func addWebhookRecord(webhook *Webhook, record *casvisorsdk.Record, statusCode int, respBody string, sendError error) error {
if statusCode == 200 {
return nil
}
if len(respBody) > 300 {
respBody = respBody[0:300]
}
webhookRecord := &casvisorsdk.Record{
Owner: record.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Organization: record.Organization,
User: record.User,
Method: webhook.Method,
Action: "send-webhook",
RequestUri: webhook.Url,
StatusCode: statusCode,
Response: respBody,
Language: record.Language,
IsTriggered: false,
}
if sendError != nil {
webhookRecord.Response = sendError.Error()
}
_, err := addRecord(webhookRecord)
return err
}
func SendWebhooks(record *casvisorsdk.Record) error { func SendWebhooks(record *casvisorsdk.Record) error {
webhooks, err := getWebhooksByOrganization("") webhooks, err := getWebhooksByOrganization("")
if err != nil { if err != nil {
@ -236,11 +287,16 @@ func SendWebhooks(record *casvisorsdk.Record) error {
} }
} }
err = sendWebhook(webhook, record, user) statusCode, respBody, err := sendWebhook(webhook, record, user)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
continue
} }
err = addWebhookRecord(webhook, record, statusCode, respBody, err)
if err != nil {
errs = append(errs, err)
}
} }
if len(errs) > 0 { if len(errs) > 0 {

View File

@ -238,6 +238,15 @@ func AddRolesInBatch(roles []*Role) bool {
return affected return affected
} }
func deleteRole(role *Role) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{role.Owner, role.Name}).Delete(&Role{})
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteRole(role *Role) (bool, error) { func DeleteRole(role *Role) (bool, error) {
roleId := role.GetId() roleId := role.GetId()
permissions, err := GetPermissionsByRole(roleId) permissions, err := GetPermissionsByRole(roleId)
@ -253,12 +262,7 @@ func DeleteRole(role *Role) (bool, error) {
} }
} }
affected, err := ormer.Engine.ID(core.PK{role.Owner, role.Name}).Delete(&Role{}) return deleteRole(role)
if err != nil {
return false, err
}
return affected != 0, nil
} }
func (role *Role) GetId() string { func (role *Role) GetId() string {

View File

@ -155,7 +155,8 @@ func GetMaskedSyncers(syncers []*Syncer, errs ...error) ([]*Syncer, error) {
func UpdateSyncer(id string, syncer *Syncer) (bool, error) { func UpdateSyncer(id string, syncer *Syncer) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if s, err := getSyncer(owner, name); err != nil { s, err := getSyncer(owner, name)
if err != nil {
return false, err return false, err
} else if s == nil { } else if s == nil {
return false, nil return false, nil
@ -163,7 +164,7 @@ func UpdateSyncer(id string, syncer *Syncer) (bool, error) {
session := ormer.Engine.ID(core.PK{owner, name}).AllCols() session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
if syncer.Password == "***" { if syncer.Password == "***" {
session.Omit("password") syncer.Password = s.Password
} }
affected, err := session.Update(syncer) affected, err := session.Update(syncer)
if err != nil { if err != nil {

View File

@ -142,9 +142,11 @@ func (syncer *Syncer) syncUsers() error {
} }
} }
_, err = AddUsersInBatch(newUsers) if len(newUsers) != 0 {
if err != nil { _, err = AddUsersInBatch(newUsers)
return err if err != nil {
return err
}
} }
if !syncer.IsReadOnly { if !syncer.IsReadOnly {

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

@ -203,7 +203,8 @@ 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"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
} }
type Userinfo struct { type Userinfo struct {
@ -675,18 +676,18 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"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",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "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",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox", "auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"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")
@ -833,6 +834,11 @@ func AddUser(user *User) (bool, error) {
} }
} }
isUsernameLowered := conf.GetConfigBool("isUsernameLowered")
if isUsernameLowered {
user.Name = strings.ToLower(user.Name)
}
affected, err := ormer.Engine.Insert(user) affected, err := ormer.Engine.Insert(user)
if err != nil { if err != nil {
return false, err return false, err
@ -846,6 +852,8 @@ func AddUsers(users []*User) (bool, error) {
return false, fmt.Errorf("no users are provided") return false, fmt.Errorf("no users are provided")
} }
isUsernameLowered := conf.GetConfigBool("isUsernameLowered")
// organization := GetOrganizationByUser(users[0]) // organization := GetOrganizationByUser(users[0])
for _, user := range users { for _, user := range users {
// this function is only used for syncer or batch upload, so no need to encrypt the password // this function is only used for syncer or batch upload, so no need to encrypt the password
@ -869,6 +877,11 @@ func AddUsers(users []*User) (bool, error) {
return false, err return false, err
} }
} }
user.Name = strings.TrimSpace(user.Name)
if isUsernameLowered {
user.Name = strings.ToLower(user.Name)
}
} }
affected, err := ormer.Engine.Insert(users) affected, err := ormer.Engine.Insert(users)
@ -908,6 +921,15 @@ func AddUsersInBatch(users []*User) (bool, error) {
return affected, nil return affected, nil
} }
func deleteUser(user *User) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{user.Owner, user.Name}).Delete(&User{})
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteUser(user *User) (bool, error) { func DeleteUser(user *User) (bool, error) {
// Forced offline the user first // Forced offline the user first
_, err := DeleteSession(util.GetSessionId(user.Owner, user.Name, CasdoorApplication)) _, err := DeleteSession(util.GetSessionId(user.Owner, user.Name, CasdoorApplication))
@ -915,12 +937,7 @@ func DeleteUser(user *User) (bool, error) {
return false, err return false, err
} }
affected, err := ormer.Engine.ID(core.PK{user.Owner, user.Name}).Delete(&User{}) return deleteUser(user)
if err != nil {
return false, err
}
return affected != 0, nil
} }
func GetUserInfo(user *User, scope string, aud string, host string) (*Userinfo, error) { func GetUserInfo(user *User, scope string, aud string, host string) (*Userinfo, error) {
@ -992,7 +1009,7 @@ func (user *User) GetFriendlyName() string {
} }
func isUserIdGlobalAdmin(userId string) bool { func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/") || strings.HasPrefix(userId, "app/") return strings.HasPrefix(userId, "built-in/") || IsAppUser(userId)
} }
func ExtendUserWithRolesAndPermissions(user *User) (err error) { func ExtendUserWithRolesAndPermissions(user *User) (err error) {
@ -1140,3 +1157,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

@ -21,12 +21,11 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
jsoniter "github.com/json-iterator/go"
"github.com/casdoor/casdoor/idp" "github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
jsoniter "github.com/json-iterator/go"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
@ -57,6 +56,13 @@ func HasUserByField(organizationName string, field string, value string) bool {
} }
func GetUserByFields(organization string, field string) (*User, error) { func GetUserByFields(organization string, field string) (*User, error) {
isUsernameLowered := conf.GetConfigBool("isUsernameLowered")
if isUsernameLowered {
field = strings.ToLower(field)
}
field = strings.TrimSpace(field)
// check username // check username
user, err := GetUserByField(organization, "name", field) user, err := GetUserByField(organization, "name", field)
if err != nil || user != nil { if err != nil || user != nil {
@ -405,6 +411,15 @@ 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)
@ -464,3 +479,10 @@ func (user *User) IsAdminUser() bool {
return user.IsAdmin || user.IsGlobalAdmin() return user.IsAdmin || user.IsGlobalAdmin()
} }
func IsAppUser(userId string) bool {
if strings.HasPrefix(userId, "app/") {
return true
}
return false
}

View File

@ -15,6 +15,7 @@
package object package object
import ( import (
"io"
"net/http" "net/http"
"strings" "strings"
@ -22,7 +23,7 @@ import (
"github.com/casvisor/casvisor-go-sdk/casvisorsdk" "github.com/casvisor/casvisor-go-sdk/casvisorsdk"
) )
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) error { func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
client := &http.Client{} client := &http.Client{}
type RecordEx struct { type RecordEx struct {
@ -38,7 +39,7 @@ func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *Use
req, err := http.NewRequest(webhook.Method, webhook.Url, body) req, err := http.NewRequest(webhook.Method, webhook.Url, body)
if err != nil { if err != nil {
return err return 0, "", err
} }
req.Header.Set("Content-Type", webhook.ContentType) req.Header.Set("Content-Type", webhook.ContentType)
@ -47,6 +48,15 @@ func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *Use
req.Header.Set(header.Name, header.Value) req.Header.Set(header.Name, header.Value)
} }
_, err = client.Do(req) resp, err := client.Do(req)
return err if err != nil {
return 0, "", err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return 0, "", err
}
return resp.StatusCode, string(bodyBytes), err
} }

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
} }

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

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

View File

@ -70,6 +70,7 @@ func (sp LocalFileSystemProvider) Put(path string, reader io.Reader) (*oss.Objec
dst, err := os.Create(filepath.Clean(fullPath)) dst, err := os.Create(filepath.Clean(fullPath))
if err == nil { if err == nil {
defer dst.Close()
if seeker, ok := reader.(io.ReadSeeker); ok { if seeker, ok := reader.(io.ReadSeeker); ok {
seeker.Seek(0, 0) seeker.Seek(0, 0)
} }

View File

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "1.16.1", "@ant-design/cssinjs": "^1.10.1",
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@craco/craco": "^6.4.5", "@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10", "@crowdin/cli": "^3.7.10",

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 {setIsTourVisible, 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);
setIsTourVisible(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}`);
@ -374,6 +377,7 @@ class App extends Component {
}); });
}} }}
onLoginSuccess={(redirectUrl) => { onLoginSuccess={(redirectUrl) => {
window.google?.accounts?.id?.cancel();
if (redirectUrl) { if (redirectUrl) {
localStorage.setItem("mfaRedirectUrl", redirectUrl); localStorage.setItem("mfaRedirectUrl", redirectUrl);
} }
@ -413,6 +417,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

@ -22,6 +22,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
import {SignupTableDefaultCssMap} from "./table/SignupTable";
class ApplicationListPage extends BaseListPage { class ApplicationListPage extends BaseListPage {
constructor(props) { constructor(props) {
@ -61,6 +62,8 @@ class ApplicationListPage extends BaseListPage {
{name: "Email", visible: true, required: true, rule: "Normal"}, {name: "Email", visible: true, required: true, rule: "Normal"},
{name: "Phone", visible: true, required: true, rule: "None"}, {name: "Phone", visible: true, required: true, rule: "None"},
{name: "Agreement", visible: true, required: true, rule: "None"}, {name: "Agreement", visible: true, required: true, rule: "None"},
{name: "Signup button", visible: true, required: true, rule: "None"},
{name: "Providers", visible: true, required: true, rule: "None", customCss: SignupTableDefaultCssMap["Providers"]},
], ],
grantTypes: ["authorization_code", "password", "client_credentials", "token", "id_token", "refresh_token"], grantTypes: ["authorization_code", "password", "client_credentials", "token", "id_token", "refresh_token"],
cert: "cert-built-in", cert: "cert-built-in",

View File

@ -108,8 +108,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} />)} />

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

@ -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"},

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);
@ -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

@ -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";
@ -41,7 +41,7 @@ class ProductEditPage extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getProduct(); this.getProduct();
this.getOrganizations(); this.getOrganizations();
this.getPaymentProviders(); this.getPaymentProviders(this.state.organizationName);
} }
getProduct() { getProduct() {
@ -67,8 +67,8 @@ class ProductEditPage extends React.Component {
}); });
} }
getPaymentProviders() { getPaymentProviders(organizationName) {
ProviderBackend.getProviders(this.props.account.owner) ProviderBackend.getProviders(organizationName)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.setState({ this.setState({
@ -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",
}; };

View File

@ -244,7 +244,7 @@ class ProviderEditPage extends React.Component {
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"));
} }
case "Email": case "Email":
if (provider.type === "Azure ACS") { if (provider.type === "Azure ACS" || provider.type === "SendGrid") {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip")); return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
} else { } else {
return Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip")); return Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"));
@ -729,7 +729,7 @@ class ProviderEditPage extends React.Component {
<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") ||
(this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Line" || this.state.provider.type === "Telegram" || this.state.provider.type === "Bark" || this.state.provider.type === "Discord" || this.state.provider.type === "Slack" || this.state.provider.type === "Pushbullet" || this.state.provider.type === "Pushover" || this.state.provider.type === "Lark" || this.state.provider.type === "Microsoft Teams")) ? null : ( (this.state.provider.category === "Notification" && (this.state.provider.type === "Line" || this.state.provider.type === "Telegram" || this.state.provider.type === "Bark" || this.state.provider.type === "Discord" || this.state.provider.type === "Slack" || this.state.provider.type === "Pushbullet" || this.state.provider.type === "Pushover" || this.state.provider.type === "Lark" || this.state.provider.type === "Microsoft Teams")) ? 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}>
@ -770,7 +770,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
{ {
(this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") ? null : ( (this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? 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}>
{this.getClientSecret2Label(this.state.provider)} : {this.getClientSecret2Label(this.state.provider)} :
@ -828,6 +828,20 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) )
} }
{
this.state.provider.type !== "Google" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Get phone number"), i18next.t("provider:Get phone number - Tooltip"))} :
</Col>
<Col span={1} >
<Switch disabled={!this.state.provider.clientId} checked={this.state.provider.disableSsl} onChange={checked => {
this.updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
)
}
{ {
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.provider.type !== "Okta" ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@ -985,17 +999,19 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) : this.state.provider.category === "Email" ? ( ) : this.state.provider.category === "Email" ? (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > {["SendGrid"].includes(this.state.provider.type) ? null : (
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Row style={{marginTop: "20px"}} >
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} : <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col> {Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
<Col span={22} > </Col>
<Input prefix={<LinkOutlined />} value={this.state.provider.host} onChange={e => { <Col span={22} >
this.updateProviderField("host", e.target.value); <Input prefix={<LinkOutlined />} value={this.state.provider.host} onChange={e => {
}} /> this.updateProviderField("host", e.target.value);
</Col> }} />
</Row> </Col>
{["Azure ACS"].includes(this.state.provider.type) ? null : ( </Row>
)}
{["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
@ -1007,7 +1023,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Azure ACS"].includes(this.state.provider.type) ? null : ( {["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
@ -1073,7 +1089,7 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("receiver", e.target.value); this.updateProviderField("receiver", e.target.value);
}} /> }} />
</Col> </Col>
{["Azure ACS"].includes(this.state.provider.type) ? null : ( {["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} > <Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test SMTP Connection")} {i18next.t("provider:Test SMTP Connection")}
</Button> </Button>

View File

@ -151,6 +151,14 @@ class RecordListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("language"), ...this.getColumnSearchProps("language"),
}, },
{
title: i18next.t("record:Status code"),
dataIndex: "statusCode",
key: "statusCode",
width: "90px",
sorter: true,
...this.getColumnSearchProps("statusCode"),
},
{ {
title: i18next.t("record:Response"), title: i18next.t("record:Response"),
dataIndex: "response", dataIndex: "response",

View File

@ -181,6 +181,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_azure.png`, logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://learn.microsoft.com/zh-cn/azure/communication-services", url: "https://learn.microsoft.com/zh-cn/azure/communication-services",
}, },
"SendGrid": {
logo: `${StaticBaseUrl}/img/email_sendgrid.png`,
url: "https://sendgrid.com/",
},
"Custom HTTP Email": { "Custom HTTP Email": {
logo: `${StaticBaseUrl}/img/social_default.png`, logo: `${StaticBaseUrl}/img/social_default.png`,
url: "https://casdoor.org/docs/provider/email/overview", url: "https://casdoor.org/docs/provider/email/overview",
@ -1015,6 +1019,7 @@ export function getProviderTypeOptions(category) {
{id: "SUBMAIL", name: "SUBMAIL"}, {id: "SUBMAIL", name: "SUBMAIL"},
{id: "Mailtrap", name: "Mailtrap"}, {id: "Mailtrap", name: "Mailtrap"},
{id: "Azure ACS", name: "Azure ACS"}, {id: "Azure ACS", name: "Azure ACS"},
{id: "SendGrid", name: "SendGrid"},
{id: "Custom HTTP Email", name: "Custom HTTP Email"}, {id: "Custom HTTP Email", name: "Custom HTTP Email"},
] ]
); );
@ -1458,7 +1463,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() {
@ -1511,3 +1516,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

@ -208,6 +208,12 @@ export function setIsTourVisible(visible) {
window.dispatchEvent(new Event("storageTourChanged")); 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";
} }

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, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd"; import {Button, Card, Col, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag, Tooltip} from "antd";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import {TotpMfaType} from "./auth/MfaSetupPage"; import {TotpMfaType} from "./auth/MfaSetupPage";
import * as GroupBackend from "./backend/GroupBackend"; import * as GroupBackend from "./backend/GroupBackend";
@ -202,7 +202,7 @@ class UserEditPage extends React.Component {
return value; return value;
} }
updateUserField(key, value) { updateUserField(key, value, idx) {
if (this.props.account === null) { if (this.props.account === null) {
return; return;
} }
@ -210,7 +210,15 @@ class UserEditPage extends React.Component {
value = this.parseUserField(key, value); value = this.parseUserField(key, value);
const user = this.state.user; const user = this.state.user;
user[key] = value; if (key === "address") {
if (!user[key]) {
user[key] = ["", ""];
}
user[key][idx] = value;
} else {
user[key] = value;
}
this.setState({ this.setState({
user: user, user: user,
}); });
@ -407,7 +415,17 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} : {Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<PasswordModal user={this.state.user} userName={this.state.userName} organization={this.getUserOrganization()} account={this.props.account} disabled={disabled} /> {
(this.state.user.name === this.state.userName) ? (
<PasswordModal user={this.state.user} userName={this.state.userName} organization={this.getUserOrganization()} account={this.props.account} disabled={disabled} />
) : (
<Tooltip placement={"topLeft"} title={i18next.t("user:You have changed the username, please save your change first before modifying the password")}>
<span>
<PasswordModal user={this.state.user} userName={this.state.userName} organization={this.getUserOrganization()} account={this.props.account} disabled={true} />
</span>
</Tooltip>
)
}
</Col> </Col>
</Row> </Row>
); );
@ -491,16 +509,33 @@ class UserEditPage extends React.Component {
); );
} else if (accountItem.name === "Address") { } else if (accountItem.name === "Address") {
return ( return (
<Row style={{marginTop: "20px"}} > <React.Fragment>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Row style={{marginTop: "20px"}} >
{Setting.getLabel(i18next.t("user:Address"), i18next.t("user:Address - Tooltip"))} : <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col> {Setting.getLabel(i18next.t("user:Address"), i18next.t("user:Address - Tooltip"))} :
<Col span={22} > </Col>
<Input value={this.state.user.address} onChange={e => { <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
this.updateUserField("address", e.target.value); <span>{i18next.t("user:Address line") + " 1"}</span> :
}} /> </Col>
</Col> <Col span={20} >
</Row> <Input value={!this.state.user.address ? "" : this.state.user.address[0]} onChange={e => {
this.updateUserField("address", e.target.value, 0);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
<span>{i18next.t("user:Address line") + " 2"}</span> :
</Col>
<Col span={20} >
<Input value={!this.state.user.address ? "" : this.state.user.address[1]} onChange={e => {
this.updateUserField("address", e.target.value, 1);
}} />
</Col>
</Row>
</React.Fragment>
); );
} else if (accountItem.name === "Affiliation") { } else if (accountItem.name === "Affiliation") {
return ( return (
@ -672,6 +707,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"}} >
@ -991,6 +1039,19 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </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

@ -19,7 +19,7 @@ import * as VerificationBackend from "./backend/VerificationBackend";
import i18next from "i18next"; import i18next from "i18next";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import React from "react"; import React from "react";
import {Table} from "antd"; import {Switch, Table} from "antd";
class VerificationListPage extends BaseListPage { class VerificationListPage extends BaseListPage {
newVerification() { newVerification() {
@ -35,26 +35,48 @@ class VerificationListPage extends BaseListPage {
renderTable(verifications) { renderTable(verifications) {
const columns = [ const columns = [
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Organization"),
dataIndex: "name", dataIndex: "owner",
key: "name", key: "owner",
width: "150px", width: "120px",
fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps("name"), ...this.getColumnSearchProps("owner"),
render: (text, record, index) => { render: (text, record, index) => {
if (text === "admin") {
return `(${i18next.t("general:empty")})`;
}
return ( return (
<Link to={`/syncers/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
); );
}, },
}, },
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "260px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{ {
title: i18next.t("provider:Type"), title: i18next.t("provider:Type"),
dataIndex: "type", dataIndex: "type",
key: "type", key: "type",
width: "120px", width: "90px",
sorter: true, sorter: true,
...this.getColumnSearchProps("type"), ...this.getColumnSearchProps("type"),
}, },
@ -67,7 +89,7 @@ class VerificationListPage extends BaseListPage {
...this.getColumnSearchProps("user"), ...this.getColumnSearchProps("user"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/users/${record.owner}/${text}`}> <Link to={`/users/${text}`}>
{text} {text}
</Link> </Link>
); );
@ -88,6 +110,26 @@ class VerificationListPage extends BaseListPage {
); );
}, },
}, },
{
title: i18next.t("general:Client IP"),
dataIndex: "remoteAddr",
key: "remoteAddr",
width: "100px",
sorter: true,
...this.getColumnSearchProps("remoteAddr"),
render: (text, record, index) => {
let clientIp = text;
if (clientIp.endsWith(": ")) {
clientIp = clientIp.slice(0, -2);
}
return (
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${clientIp}`}>
{clientIp}
</a>
);
},
},
{ {
title: i18next.t("verification:Receiver"), title: i18next.t("verification:Receiver"),
dataIndex: "receiver", dataIndex: "receiver",
@ -100,28 +142,20 @@ class VerificationListPage extends BaseListPage {
title: i18next.t("login:Verification code"), title: i18next.t("login:Verification code"),
dataIndex: "code", dataIndex: "code",
key: "code", key: "code",
width: "120px", width: "150px",
sorter: true, sorter: true,
...this.getColumnSearchProps("code"), ...this.getColumnSearchProps("code"),
}, },
{ {
title: i18next.t("general:Timestamp"), title: i18next.t("verification:Is used"),
dataIndex: "time", dataIndex: "isUsed",
key: "time", key: "isUsed",
width: "160px", width: "90px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text * 1000); return (
}, <Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
}, );
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}, },
}, },
]; ];
@ -156,7 +190,7 @@ class VerificationListPage extends BaseListPage {
value = params.type; value = params.type;
} }
this.setState({loading: true}); this.setState({loading: true});
VerificationBackend.getVerifications("admin", Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) VerificationBackend.getVerifications("", Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => { .then((res) => {
this.setState({ this.setState({
loading: false, loading: false,

View File

@ -153,21 +153,37 @@ class AuthCallback extends React.Component {
// OAuth // OAuth
const oAuthParams = Util.getOAuthGetParameters(innerParams); const oAuthParams = Util.getOAuthGetParameters(innerParams);
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?"; const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
const signinUrl = localStorage.getItem("signinUrl");
AuthBackend.login(body, oAuthParams) AuthBackend.login(body, oAuthParams)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const responseType = this.getResponseType(); const responseType = this.getResponseType();
if (responseType === "login") { if (responseType === "login") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
Setting.showMessage("success", "Logged in successfully"); Setting.showMessage("success", "Logged in successfully");
// Setting.goToLinkSoft(this, "/"); // Setting.goToLinkSoft(this, "/");
const link = Setting.getFromLink(); const link = Setting.getFromLink();
Setting.goToLink(link); Setting.goToLink(link);
} else if (responseType === "code") { } else if (responseType === "code") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const code = res.data; const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`); Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
// Setting.showMessage("success", `Authorization code: ${res.data}`); // Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") { } else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const token = res.data; const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`); Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "link") { } else if (responseType === "link") {
@ -181,6 +197,11 @@ class AuthCallback extends React.Component {
relayState: oAuthParams.relayState, relayState: oAuthParams.relayState,
}); });
} else { } else {
if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
const SAMLResponse = res.data; const SAMLResponse = res.data;
const redirectUri = res.data2.redirectUrl; const redirectUri = res.data2.redirectUrl;
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`); Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);

View File

@ -21,7 +21,7 @@ import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import {SendCodeInput} from "../common/SendCodeInput"; import {SendCodeInput} from "../common/SendCodeInput";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import {CheckCircleOutlined, KeyOutlined, LockOutlined, SolutionOutlined, UserOutlined} from "@ant-design/icons"; import {ArrowLeftOutlined, CheckCircleOutlined, KeyOutlined, LockOutlined, SolutionOutlined, UserOutlined} from "@ant-design/icons";
import CustomGithubCorner from "../common/CustomGithubCorner"; import CustomGithubCorner from "../common/CustomGithubCorner";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import * as PasswordChecker from "../common/PasswordChecker"; import * as PasswordChecker from "../common/PasswordChecker";
@ -35,8 +35,8 @@ class ForgetPage extends React.Component {
classes: props, classes: props,
applicationName: props.applicationName ?? props.match.params?.applicationName, applicationName: props.applicationName ?? props.match.params?.applicationName,
msg: null, msg: null,
name: "", name: props.account ? props.account.name : "",
username: "", username: props.account ? props.account.name : "",
phone: "", phone: "",
email: "", email: "",
dest: "", dest: "",
@ -44,7 +44,6 @@ class ForgetPage extends React.Component {
verifyType: "", // "email", "phone" verifyType: "", // "email", "phone"
current: 0, current: 0,
}; };
this.form = React.createRef(); this.form = React.createRef();
} }
@ -205,6 +204,7 @@ class ForgetPage extends React.Component {
initialValues={{ initialValues={{
application: application.name, application: application.name,
organization: application.organization, organization: application.organization,
username: this.state.name,
}} }}
style={{width: "300px"}} style={{width: "300px"}}
size="large" size="large"
@ -443,6 +443,18 @@ class ForgetPage extends React.Component {
); );
} }
stepBack() {
if (this.state.current > 0) {
this.setState({
current: this.state.current - 1,
});
} else if (this.props.history.length > 1) {
this.props.history.goBack();
} else {
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
}
}
render() { render() {
const application = this.getApplicationObj(); const application = this.getApplicationObj();
if (application === undefined) { if (application === undefined) {
@ -456,6 +468,9 @@ class ForgetPage extends React.Component {
<React.Fragment> <React.Fragment>
<CustomGithubCorner /> <CustomGithubCorner />
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}> <div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
<Button type="text" style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}} size={"large"} onClick={() => {this.stepBack();}}>
<ArrowLeftOutlined style={{fontSize: "24px"}} />
</Button>
<Row> <Row>
<Col span={24} style={{justifyContent: "center"}}> <Col span={24} style={{justifyContent: "center"}}>
<Row> <Row>
@ -473,7 +488,7 @@ class ForgetPage extends React.Component {
<Row> <Row>
<Col span={24}> <Col span={24}>
<div style={{textAlign: "center", fontSize: "28px"}}> <div style={{textAlign: "center", fontSize: "28px"}}>
{i18next.t("forget:Retrieve password")} {i18next.t("forget:Reset password")}
</div> </div>
</Col> </Col>
</Row> </Row>

View File

@ -52,7 +52,7 @@ export function GoogleOneTapLoginVirtualButton(prop) {
redirectUri = `${redirectUri}?state=${state}&code=${encodeURIComponent(code)}`; redirectUri = `${redirectUri}?state=${state}&code=${encodeURIComponent(code)}`;
Setting.goToLink(redirectUri); Setting.goToLink(redirectUri);
}, },
disableCancelOnUnmount: true, disableCancelOnUnmount: false,
}); });
} }

View File

@ -68,6 +68,8 @@ class LoginPage extends React.Component {
this.state.applicationName = props.match?.params?.casApplicationName; this.state.applicationName = props.match?.params?.casApplicationName;
} }
localStorage.setItem("signinUrl", window.location.href);
this.form = React.createRef(); this.form = React.createRef();
} }
@ -300,6 +302,12 @@ class LoginPage extends React.Component {
return; return;
} }
if (resp.data2) {
sessionStorage.setItem("signinUrl", window.location.href);
Setting.goToLinkSoft(ths, `/forget/${application.name}`);
return;
}
if (Setting.hasPromptPage(application)) { if (Setting.hasPromptPage(application)) {
AuthBackend.getAccount() AuthBackend.getAccount()
.then((res) => { .then((res) => {
@ -442,15 +450,27 @@ class LoginPage extends React.Component {
const responseType = values["type"]; const responseType = values["type"];
if (responseType === "login") { if (responseType === "login") {
if (res.data2) {
sessionStorage.setItem("signinUrl", window.location.href);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
}
Setting.showMessage("success", i18next.t("application:Logged in successfully")); Setting.showMessage("success", i18next.t("application:Logged in successfully"));
this.props.onLoginSuccess(); this.props.onLoginSuccess();
} else if (responseType === "code") { } else if (responseType === "code") {
this.postCodeLoginAction(res); this.postCodeLoginAction(res);
} else if (responseType === "token" || responseType === "id_token") { } else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
sessionStorage.setItem("signinUrl", window.location.href);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
}
const amendatoryResponseType = responseType === "token" ? "access_token" : responseType; const amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
const accessToken = res.data; const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`); Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "saml") { } else if (responseType === "saml") {
if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", window.location.href);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
}
if (res.data2.method === "POST") { if (res.data2.method === "POST") {
this.setState({ this.setState({
samlResponse: res.data, samlResponse: res.data,
@ -532,7 +552,7 @@ class LoginPage extends React.Component {
if (signinItem.name === "Logo") { if (signinItem.name === "Logo") {
return ( return (
<div className="login-logo-box"> <div className="login-logo-box">
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{ {
Setting.renderHelmet(application) Setting.renderHelmet(application)
} }
@ -543,8 +563,8 @@ class LoginPage extends React.Component {
); );
} else if (signinItem.name === "Back button") { } else if (signinItem.name === "Back button") {
return ( return (
<div> <div className="back-button">
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{ {
this.renderBackButton() this.renderBackButton()
} }
@ -562,24 +582,26 @@ class LoginPage extends React.Component {
return ( return (
<div className="login-languages"> <div className="login-languages">
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<LanguageSelect languages={application.organizationObj.languages} /> <LanguageSelect languages={application.organizationObj.languages} />
</div> </div>
); );
} else if (signinItem.name === "Signin methods") { } else if (signinItem.name === "Signin methods") {
return ( return (
<div> <div>
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{this.renderMethodChoiceBox()} {this.renderMethodChoiceBox()}
</div> </div>
) )
; ;
} else if (signinItem.name === "Username") { } else if (signinItem.name === "Username") {
return ( return (
<div className="login-username"> <div>
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Form.Item <Form.Item
name="username" name="username"
className="login-username"
label={signinItem.label ? signinItem.label : null}
rules={[ rules={[
{ {
required: true, required: true,
@ -637,6 +659,7 @@ class LoginPage extends React.Component {
<Input <Input
id="input" id="input"
className="login-username-input"
prefix={<UserOutlined className="site-form-item-icon" />} prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.getPlaceholder()} placeholder={this.getPlaceholder()}
onChange={e => { onChange={e => {
@ -651,14 +674,14 @@ class LoginPage extends React.Component {
} else if (signinItem.name === "Password") { } else if (signinItem.name === "Password") {
return ( return (
<div> <div>
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{this.renderPasswordOrCodeInput()} {this.renderPasswordOrCodeInput(signinItem)}
</div> </div>
); );
} else if (signinItem.name === "Forgot password?") { } else if (signinItem.name === "Forgot password?") {
return ( return (
<div> <div>
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<div className="login-forget-password"> <div className="login-forget-password">
<Form.Item name="autoSignin" valuePropName="checked" noStyle> <Form.Item name="autoSignin" valuePropName="checked" noStyle>
<Checkbox style={{float: "left"}}> <Checkbox style={{float: "left"}}>
@ -666,7 +689,7 @@ class LoginPage extends React.Component {
</Checkbox> </Checkbox>
</Form.Item> </Form.Item>
{ {
signinItem.visible ? Setting.renderForgetLink(application, i18next.t("login:Forgot password?")) : null signinItem.visible ? Setting.renderForgetLink(application, signinItem.label ? signinItem.label : i18next.t("login:Forgot password?")) : null
} }
</div> </div>
</div> </div>
@ -676,7 +699,7 @@ class LoginPage extends React.Component {
} else if (signinItem.name === "Login button") { } else if (signinItem.name === "Login button") {
return ( return (
<Form.Item className="login-button-box"> <Form.Item className="login-button-box">
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"
@ -685,7 +708,7 @@ class LoginPage extends React.Component {
{ {
this.state.loginMethod === "webAuthn" ? i18next.t("login:Sign in with WebAuthn") : this.state.loginMethod === "webAuthn" ? i18next.t("login:Sign in with WebAuthn") :
this.state.loginMethod === "faceId" ? i18next.t("login:Sign in with Face ID") : this.state.loginMethod === "faceId" ? i18next.t("login:Sign in with Face ID") :
i18next.t("login:Sign In") signinItem.label ? signinItem.label : i18next.t("login:Sign In")
} }
</Button> </Button>
{ {
@ -720,7 +743,7 @@ class LoginPage extends React.Component {
return ( return (
<div> <div>
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Form.Item> <Form.Item>
{ {
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => { application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
@ -735,13 +758,13 @@ class LoginPage extends React.Component {
); );
} else if (signinItem.name.startsWith("Text ") || signinItem?.isCustom) { } else if (signinItem.name.startsWith("Text ") || signinItem?.isCustom) {
return ( return (
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: signinItem.customCss}} />
); );
} else if (signinItem.name === "Signup link") { } else if (signinItem.name === "Signup link") {
return ( return (
<div style={{width: "100%"}} className="login-signup-link"> <div style={{width: "100%"}} className="login-signup-link">
<div dangerouslySetInnerHTML={{__html: signinItem.label}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{this.renderFooter(application)} {this.renderFooter(application, signinItem)}
</div> </div>
); );
} }
@ -838,7 +861,7 @@ class LoginPage extends React.Component {
{application.displayName} {application.displayName}
</span> </span>
</a> </a>
: :
</div> </div>
<br /> <br />
{ {
@ -894,17 +917,20 @@ class LoginPage extends React.Component {
/>; />;
} }
renderFooter(application) { renderFooter(application, signinItem) {
return ( return (
<div> <div>
{ {
!application.enableSignUp ? null : ( !application.enableSignUp ? null : (
<React.Fragment> signinItem.label ? Setting.renderSignupLink(application, signinItem.label) :
{i18next.t("login:No account?")}&nbsp; (
{ <React.Fragment>
Setting.renderSignupLink(application, i18next.t("login:sign up now")) {i18next.t("login:No account?")}
} {
</React.Fragment> Setting.renderSignupLink(application, i18next.t("login:sign up now"))
}
</React.Fragment>
)
) )
} }
</div> </div>
@ -1020,17 +1046,20 @@ class LoginPage extends React.Component {
}); });
} }
renderPasswordOrCodeInput() { renderPasswordOrCodeInput(signinItem) {
const application = this.getApplicationObj(); const application = this.getApplicationObj();
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") { if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
return ( return (
<Col span={24}> <Col span={24}>
<div className="login-password"> <div>
<Form.Item <Form.Item
name="password" name="password"
className="login-password"
label={signinItem.label ? signinItem.label : null}
rules={[{required: true, message: i18next.t("login:Please input your password!")}]} rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
> >
<Input.Password <Input.Password
className="login-password-input"
prefix={<LockOutlined className="site-form-item-icon" />} prefix={<LockOutlined className="site-form-item-icon" />}
type="password" type="password"
placeholder={i18next.t("general:Password")} placeholder={i18next.t("general:Password")}
@ -1173,7 +1202,7 @@ class LoginPage extends React.Component {
}; };
return ( return (
<div style={{height: 300}}> <div style={{height: 300, minWidth: 320}}>
{renderChoiceBox()} {renderChoiceBox()}
</div> </div>
); );
@ -1195,8 +1224,7 @@ class LoginPage extends React.Component {
renderBackButton() { renderBackButton() {
if (this.state.orgChoiceMode === "None" || this.props.preview === "auto") { if (this.state.orgChoiceMode === "None" || this.props.preview === "auto") {
return ( return (
<Button type="text" size="large" icon={<ArrowLeftOutlined />} <Button className="back-inner-button" type="text" size="large" icon={<ArrowLeftOutlined />}
className="back-button"
onClick={() => history.back()}> onClick={() => history.back()}>
</Button> </Button>
); );

View File

@ -384,8 +384,7 @@ export function getAuthUrl(application, provider, method, code) {
let endpoint = authInfo[provider.type].endpoint; let endpoint = authInfo[provider.type].endpoint;
let redirectUri = `${window.location.origin}/callback`; let redirectUri = `${window.location.origin}/callback`;
const scope = authInfo[provider.type].scope; let scope = authInfo[provider.type].scope;
const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter"); const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter");
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState); const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState);
const codeChallenge = "P3S-a7dr8bgM4bF6vOyiKkKETDl16rcAzao9F8UIL1Y"; // SHA256(Base64-URL-encode("casdoor-verifier")) const codeChallenge = "P3S-a7dr8bgM4bF6vOyiKkKETDl16rcAzao9F8UIL1Y"; // SHA256(Base64-URL-encode("casdoor-verifier"))
@ -396,9 +395,11 @@ export function getAuthUrl(application, provider, method, code) {
} }
} else if (provider.type === "Apple") { } else if (provider.type === "Apple") {
redirectUri = `${window.location.origin}/api/callback`; redirectUri = `${window.location.origin}/api/callback`;
} else if (provider.type === "Google" && provider.disableSsl) {
scope += "+https://www.googleapis.com/auth/user.phonenumbers.read";
} }
if (provider.type === "Google" || provider.type === "GitHub" || provider.type === "QQ" || provider.type === "Facebook" if (provider.type === "Google" || provider.type === "GitHub" || provider.type === "Facebook"
|| provider.type === "Weibo" || provider.type === "Gitee" || provider.type === "LinkedIn" || provider.type === "GitLab" || provider.type === "AzureAD" || provider.type === "Weibo" || provider.type === "Gitee" || provider.type === "LinkedIn" || provider.type === "GitLab" || provider.type === "AzureAD"
|| provider.type === "Slack" || provider.type === "Line" || provider.type === "Amazon" || provider.type === "Auth0" || provider.type === "BattleNet" || provider.type === "Slack" || provider.type === "Line" || provider.type === "Amazon" || provider.type === "Auth0" || provider.type === "BattleNet"
|| provider.type === "Bitbucket" || provider.type === "Box" || provider.type === "CloudFoundry" || provider.type === "Dailymotion" || provider.type === "Bitbucket" || provider.type === "Box" || provider.type === "CloudFoundry" || provider.type === "Dailymotion"
@ -410,6 +411,8 @@ export function getAuthUrl(application, provider, method, code) {
|| provider.type === "Twitch" || provider.type === "Typetalk" || provider.type === "Uber" || provider.type === "VK" || provider.type === "Wepay" || provider.type === "Twitch" || provider.type === "Typetalk" || provider.type === "Uber" || provider.type === "VK" || provider.type === "Wepay"
|| provider.type === "Xero" || provider.type === "Yahoo" || provider.type === "Yammer" || provider.type === "Yandex" || provider.type === "Zoom") { || provider.type === "Xero" || provider.type === "Yahoo" || provider.type === "Yammer" || provider.type === "Yandex" || provider.type === "Zoom") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`; return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
} else if (provider.type === "QQ") {
return `${endpoint}?response_type=code&client_id=${provider.clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(state)}&scope=${encodeURIComponent(scope)}`;
} else if (provider.type === "AzureADB2C") { } else if (provider.type === "AzureADB2C") {
return `https://${provider.domain}.b2clogin.com/${provider.domain}.onmicrosoft.com/${provider.appId}/oauth2/v2.0/authorize?client_id=${provider.clientId}&nonce=defaultNonce&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&response_type=code&state=${state}&prompt=login`; return `https://${provider.domain}.b2clogin.com/${provider.domain}.onmicrosoft.com/${provider.appId}/oauth2/v2.0/authorize?client_id=${provider.clientId}&nonce=defaultNonce&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&response_type=code&state=${state}&prompt=login`;
} else if (provider.type === "DingTalk") { } else if (provider.type === "DingTalk") {

View File

@ -172,23 +172,23 @@ export function renderProviderLogo(provider, application, width, margin, size, l
const customAStyle = {display: "block", height: "55px", color: "#000"}; const customAStyle = {display: "block", height: "55px", color: "#000"};
const customButtonStyle = {display: "flex", alignItems: "center", width: "calc(100% - 10px)", height: "50px", margin: "5px", padding: "0 10px", backgroundColor: "transparent", boxShadow: "0px 1px 3px rgba(0,0,0,0.5)", border: "0px", borderRadius: "3px", cursor: "pointer"}; const customButtonStyle = {display: "flex", alignItems: "center", width: "calc(100% - 10px)", height: "50px", margin: "5px", padding: "0 10px", backgroundColor: "transparent", boxShadow: "0px 1px 3px rgba(0,0,0,0.5)", border: "0px", borderRadius: "3px", cursor: "pointer"};
const customImgStyle = {justfyContent: "space-between"}; const customImgStyle = {justfyContent: "space-between"};
const customSpanStyle = {textAlign: "center", lineHeight: "50px", width: "100%", fontSize: "19px"}; const customSpanStyle = {textAlign: "center", width: "100%", fontSize: "19px"};
if (provider.category === "OAuth") { if (provider.category === "OAuth") {
return ( return (
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")} style={customAStyle}> <a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")} style={customAStyle}>
<button style={customButtonStyle}> <div style={customButtonStyle}>
<img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={customImgStyle} /> <img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={customImgStyle} />
<span style={customSpanStyle}>{text}</span> <span style={customSpanStyle}>{text}</span>
</button> </div>
</a> </a>
); );
} else if (provider.category === "SAML") { } else if (provider.category === "SAML") {
return ( return (
<a key={provider.displayName} onClick={() => goToSamlUrl(provider, location)} style={customAStyle}> <a key={provider.displayName} onClick={() => goToSamlUrl(provider, location)} style={customAStyle}>
<button style={customButtonStyle}> <div style={customButtonStyle}>
<img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={customImgStyle} /> <img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={customImgStyle} />
<span style={customSpanStyle}>{text}</span> <span style={customSpanStyle}>{text}</span>
</button> </div>
</a> </a>
); );
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Form, Input, Result} from "antd"; import {Button, Form, Input, Radio, Result, Row} from "antd";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import * as ProviderButton from "./ProviderButton"; import * as ProviderButton from "./ProviderButton";
@ -71,6 +71,7 @@ class SignupPage extends React.Component {
applicationName: (props.applicationName ?? props.match?.params?.applicationName) ?? null, applicationName: (props.applicationName ?? props.match?.params?.applicationName) ?? null,
email: "", email: "",
phone: "", phone: "",
emailOrPhoneMode: "",
countryCode: "", countryCode: "",
emailCode: "", emailCode: "",
phoneCode: "", phoneCode: "",
@ -247,6 +248,7 @@ class SignupPage extends React.Component {
return ( return (
<Form.Item <Form.Item
name="username" name="username"
className="signup-username"
label={signupItem.label ? signupItem.label : i18next.t("signup:Username")} label={signupItem.label ? signupItem.label : i18next.t("signup:Username")}
rules={[ rules={[
{ {
@ -256,7 +258,8 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.username !== ""} /> <Input className="signup-username-input" placeholder={signupItem.placeholder}
disabled={this.state.invitation !== undefined && this.state.invitation.username !== ""} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Display name") { } else if (signupItem.name === "Display name") {
@ -265,6 +268,7 @@ class SignupPage extends React.Component {
<React.Fragment> <React.Fragment>
<Form.Item <Form.Item
name="firstName" name="firstName"
className="signup-first-name"
label={signupItem.label ? signupItem.label : i18next.t("general:First name")} label={signupItem.label ? signupItem.label : i18next.t("general:First name")}
rules={[ rules={[
{ {
@ -274,10 +278,11 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input className="signup-first-name-input" placeholder={signupItem.placeholder} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="lastName" name="lastName"
className="signup-last-name"
label={signupItem.label ? signupItem.label : i18next.t("general:Last name")} label={signupItem.label ? signupItem.label : i18next.t("general:Last name")}
rules={[ rules={[
{ {
@ -287,7 +292,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input className="signup-last-name-input" placeholder={signupItem.placeholder} />
</Form.Item> </Form.Item>
</React.Fragment> </React.Fragment>
); );
@ -296,6 +301,7 @@ class SignupPage extends React.Component {
return ( return (
<Form.Item <Form.Item
name="name" name="name"
className="signup-name"
label={(signupItem.label ? signupItem.label : (signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("general:Real name") : i18next.t("general:Display name"))} label={(signupItem.label ? signupItem.label : (signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("general:Real name") : i18next.t("general:Display name"))}
rules={[ rules={[
{ {
@ -305,13 +311,14 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input className="signup-name-input" placeholder={signupItem.placeholder} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Affiliation") { } else if (signupItem.name === "Affiliation") {
return ( return (
<Form.Item <Form.Item
name="affiliation" name="affiliation"
className="signup-affiliation"
label={signupItem.label ? signupItem.label : i18next.t("user:Affiliation")} label={signupItem.label ? signupItem.label : i18next.t("user:Affiliation")}
rules={[ rules={[
{ {
@ -321,13 +328,14 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input className="signup-affiliation-input" placeholder={signupItem.placeholder} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "ID card") { } else if (signupItem.name === "ID card") {
return ( return (
<Form.Item <Form.Item
name="idCard" name="idCard"
className="signup-idcard"
label={signupItem.label ? signupItem.label : i18next.t("user:ID card")} label={signupItem.label ? signupItem.label : i18next.t("user:ID card")}
rules={[ rules={[
{ {
@ -342,13 +350,14 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input className="signup-idcard-input" placeholder={signupItem.placeholder} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Country/Region") { } else if (signupItem.name === "Country/Region") {
return ( return (
<Form.Item <Form.Item
name="country_region" name="country_region"
className="signup-country-region"
label={signupItem.label ? signupItem.label : i18next.t("user:Country/Region")} label={signupItem.label ? signupItem.label : i18next.t("user:Country/Region")}
rules={[ rules={[
{ {
@ -357,137 +366,198 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<RegionSelect onChange={(value) => {this.setState({region: value});}} /> <RegionSelect className="signup-region-select" onChange={(value) => {this.setState({region: value});}} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Email") { } else if (signupItem.name === "Email" || signupItem.name === "Phone" || signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
return ( const renderEmailItem = () => {
<React.Fragment> return (
<Form.Item <React.Fragment>
name="email"
label={signupItem.label ? signupItem.label : i18next.t("general:Email")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your Email!"),
},
{
validator: (_, value) => {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.email)) {
this.setState({validEmail: false});
return Promise.reject(i18next.t("signup:The input is not valid Email!"));
}
this.setState({validEmail: true});
return Promise.resolve();
},
},
]}
>
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
</Form.Item>
{
signupItem.rule !== "No verification" &&
<Form.Item <Form.Item
name="emailCode" name="email"
label={signupItem.label ? signupItem.label : i18next.t("code:Email code")} className="signup-email"
rules={[{ label={signupItem.label ? signupItem.label : i18next.t("general:Email")}
required: required,
message: i18next.t("code:Please input your verification code!"),
}]}
>
<SendCodeInput
disabled={!this.state.validEmail}
method={"signup"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
}
</React.Fragment>
);
} else if (signupItem.name === "Phone") {
return (
<React.Fragment>
<Form.Item label={signupItem.label ? signupItem.label : i18next.t("general:Phone")} required={required}>
<Input.Group compact>
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
style={{width: "35%"}}
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
/>
</Form.Item>
<Form.Item
name="phone"
dependencies={["countryCode"]}
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
({getFieldValue}) => ({
validator: (_, value) => {
if (!required && !value) {
return Promise.resolve();
}
if (value && !Setting.isValidPhone(value, getFieldValue("countryCode"))) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
}),
]}
>
<Input
placeholder={signupItem.placeholder}
style={{width: "65%"}}
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
onChange={e => this.setState({phone: e.target.value})}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{
signupItem.rule !== "No verification" &&
<Form.Item
name="phoneCode"
label={signupItem.label ? signupItem.label : i18next.t("code:Phone code")}
rules={[ rules={[
{ {
required: required, required: required,
message: i18next.t("code:Please input your phone verification code!"), message: i18next.t("signup:Please input your Email!"),
},
{
validator: (_, value) => {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.email)) {
this.setState({validEmail: false});
return Promise.reject(i18next.t("signup:The input is not valid Email!"));
}
if (signupItem.regex) {
const reg = new RegExp(signupItem.regex);
if (!reg.test(this.state.email)) {
this.setState({validEmail: false});
return Promise.reject(i18next.t("signup:The input Email doesn't match the signup item regex!"));
}
}
this.setState({validEmail: true});
return Promise.resolve();
},
}, },
]} ]}
> >
<SendCodeInput <Input className="signup-email-input" placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
disabled={!this.state.validPhone}
method={"signup"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application}
countryCode={this.form.current?.getFieldValue("countryCode")}
/>
</Form.Item> </Form.Item>
} {
</React.Fragment> signupItem.rule !== "No verification" &&
); <Form.Item
name="emailCode"
className="signup-email-code"
label={signupItem.label ? signupItem.label : i18next.t("code:Email code")}
rules={[{
required: required,
message: i18next.t("code:Please input your verification code!"),
}]}
>
<SendCodeInput
className="signup-email-code-input"
disabled={!this.state.validEmail}
method={"signup"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
}
</React.Fragment>
);
};
const renderPhoneItem = () => {
return (
<React.Fragment>
<Form.Item className="signup-phone" label={signupItem.label ? signupItem.label : i18next.t("general:Phone")} required={required}>
<Input.Group compact>
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
style={{width: "35%"}}
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
/>
</Form.Item>
<Form.Item
name="phone"
dependencies={["countryCode"]}
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
({getFieldValue}) => ({
validator: (_, value) => {
if (!required && !value) {
return Promise.resolve();
}
if (value && !Setting.isValidPhone(value, getFieldValue("countryCode"))) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
}),
]}
>
<Input
className="signup-phone-input"
placeholder={signupItem.placeholder}
style={{width: "65%"}}
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
onChange={e => this.setState({phone: e.target.value})}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{
signupItem.rule !== "No verification" &&
<Form.Item
name="phoneCode"
className="phone-code"
label={signupItem.label ? signupItem.label : i18next.t("code:Phone code")}
rules={[
{
required: required,
message: i18next.t("code:Please input your phone verification code!"),
},
]}
>
<SendCodeInput
className="signup-phone-code-input"
disabled={!this.state.validPhone}
method={"signup"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application}
countryCode={this.form.current?.getFieldValue("countryCode")}
/>
</Form.Item>
}
</React.Fragment>
);
};
if (signupItem.name === "Email") {
return renderEmailItem();
} else if (signupItem.name === "Phone") {
return renderPhoneItem();
} else if (signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
let emailOrPhoneMode = this.state.emailOrPhoneMode;
if (emailOrPhoneMode === "") {
emailOrPhoneMode = signupItem.name === "Email or Phone" ? "Email" : "Phone";
}
return (
<React.Fragment>
<Row style={{marginTop: "30px", marginBottom: "20px"}} >
<Radio.Group style={{width: "400px"}} buttonStyle="solid" onChange={e => {
this.setState({
emailOrPhoneMode: e.target.value,
});
}} value={emailOrPhoneMode}>
{
signupItem.name === "Email or Phone" ? (
<React.Fragment>
<Radio.Button value={"Email"}>{i18next.t("general:Email")}</Radio.Button>
<Radio.Button value={"Phone"}>{i18next.t("general:Phone")}</Radio.Button>
</React.Fragment>
) : (
<React.Fragment>
<Radio.Button value={"Phone"}>{i18next.t("general:Phone")}</Radio.Button>
<Radio.Button value={"Email"}>{i18next.t("general:Email")}</Radio.Button>
</React.Fragment>
)
}
</Radio.Group>
</Row>
{
emailOrPhoneMode === "Email" ? renderEmailItem() : renderPhoneItem()
}
</React.Fragment>
);
} else {
return null;
}
} else if (signupItem.name === "Password") { } else if (signupItem.name === "Password") {
return ( return (
<Form.Item <Form.Item
name="password" name="password"
className="signup-password"
label={signupItem.label ? signupItem.label : i18next.t("general:Password")} label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
rules={[ rules={[
{ {
@ -505,13 +575,14 @@ class SignupPage extends React.Component {
]} ]}
hasFeedback hasFeedback
> >
<Input.Password placeholder={signupItem.placeholder} /> <Input.Password className="signup-password-input" placeholder={signupItem.placeholder} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Confirm password") { } else if (signupItem.name === "Confirm password") {
return ( return (
<Form.Item <Form.Item
name="confirm" name="confirm"
className="signup-confirm"
label={signupItem.label ? signupItem.label : i18next.t("signup:Confirm")} label={signupItem.label ? signupItem.label : i18next.t("signup:Confirm")}
dependencies={["password"]} dependencies={["password"]}
hasFeedback hasFeedback
@ -538,6 +609,7 @@ class SignupPage extends React.Component {
return ( return (
<Form.Item <Form.Item
name="invitationCode" name="invitationCode"
className="signup-invitation-code"
label={signupItem.label ? signupItem.label : i18next.t("application:Invitation code")} label={signupItem.label ? signupItem.label : i18next.t("application:Invitation code")}
rules={[ rules={[
{ {
@ -546,7 +618,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation !== ""} /> <Input className="signup-invitation-code-input" placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation !== ""} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Agreement") { } else if (signupItem.name === "Agreement") {
@ -555,6 +627,37 @@ class SignupPage extends React.Component {
return ( return (
<div dangerouslySetInnerHTML={{__html: signupItem.label}} /> <div dangerouslySetInnerHTML={{__html: signupItem.label}} />
); );
} else if (signupItem.name === "Signup button") {
return (
<Form.Item {...tailFormItemLayout}>
<Button type="primary" htmlType="submit" className="signup-button">
{i18next.t("account:Sign Up")}
</Button>
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp;
<a className="signup-link" onClick={() => {
const linkInStorage = sessionStorage.getItem("signinUrl");
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLinkSoft(this, linkInStorage);
} else {
Setting.redirectToLoginPage(application, this.props.history);
}
}}>
{i18next.t("signup:sign in now")}
</a>
</Form.Item>
);
} else if (signupItem.name === "Providers") {
const showForm = Setting.isPasswordEnabled(application) || Setting.isCodeSigninEnabled(application) || Setting.isWebAuthnEnabled(application) || Setting.isLdapEnabled(application);
if (signupItem.rule === "None" || signupItem.rule === "") {
signupItem.rule = showForm ? "small" : "big";
}
return (
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
return ProviderButton.renderProviderLogo(providerItem.provider, application, null, null, signupItem.rule, this.props.location);
})
);
} }
} }
@ -629,27 +732,13 @@ class SignupPage extends React.Component {
> >
</Form.Item> </Form.Item>
{ {
application.signupItems?.map(signupItem => this.renderFormItem(application, signupItem)) application.signupItems?.map((signupItem, idx) => {
} return (
<Form.Item {...tailFormItemLayout}> <div key={idx}>
<Button type="primary" htmlType="submit"> <div dangerouslySetInnerHTML={{__html: ("<style>" + signupItem.customCss + "</style>")}} />
{i18next.t("account:Sign Up")} {this.renderFormItem(application, signupItem)}
</Button> </div>
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp; );
<a onClick={() => {
const linkInStorage = sessionStorage.getItem("signinUrl");
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLinkSoft(this, linkInStorage);
} else {
Setting.redirectToLoginPage(application, this.props.history);
}
}}>
{i18next.t("signup:sign in now")}
</a>
</Form.Item>
{
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
return ProviderButton.renderProviderLogo(providerItem.provider, application, 30, 5, "small", this.props.location);
}) })
} }
</Form> </Form>
@ -662,6 +751,20 @@ class SignupPage extends React.Component {
return null; return null;
} }
let existSignupButton = false;
application.signupItems?.map(item => {
item.name === "Signup button" ? existSignupButton = true : null;
});
if (!existSignupButton) {
application.signupItems?.push({
customCss: "",
label: "",
name: "Signup button",
placeholder: "",
visible: true,
});
}
if (application.signupHtml !== "") { if (application.signupHtml !== "") {
return ( return (
<div dangerouslySetInnerHTML={{__html: application.signupHtml}} /> <div dangerouslySetInnerHTML={{__html: application.signupHtml}} />

View File

@ -70,8 +70,8 @@ export function deleteProduct(product) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "", paymentEnv = "") { export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "", paymentEnv = "", customPrice = 0) {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}&paymentEnv=${paymentEnv}`, { return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}&paymentEnv=${paymentEnv}&customPrice=${customPrice}`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@ -50,6 +50,7 @@ function testEmailProvider(provider, email = "") {
sender: provider.displayName, sender: provider.displayName,
receivers: email === "" ? ["TestSmtpServer"] : [email], receivers: email === "" ? ["TestSmtpServer"] : [email],
provider: provider.name, provider: provider.name,
providerObject: provider,
}; };
return fetch(`${Setting.ServerUrl}/api/send-email`, { return fetch(`${Setting.ServerUrl}/api/send-email`, {

View File

@ -13,11 +13,14 @@
// limitations under the License. // limitations under the License.
import {Select} from "antd"; import {Select} from "antd";
import i18next from "i18next";
import * as Setting from "../../Setting"; import * as Setting from "../../Setting";
import React from "react"; import React from "react";
const {Option} = Select;
export const CountryCodeSelect = (props) => { export const CountryCodeSelect = (props) => {
const {onChange, style, disabled, initValue} = props; const {onChange, style, disabled, initValue, mode} = props;
const countryCodes = props.countryCodes ?? []; const countryCodes = props.countryCodes ?? [];
const [value, setValue] = React.useState(""); const [value, setValue] = React.useState("");
@ -42,11 +45,19 @@ export const CountryCodeSelect = (props) => {
style={style} style={style}
disabled={disabled} disabled={disabled}
value={value} value={value}
mode={mode}
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={false}
optionLabelProp={"label"} optionLabelProp={"label"}
onChange={handleOnChange} onChange={handleOnChange}
filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())} filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())}
> >
{
props.hasDefault ? (<Option key={"All"} value={"All"} label={i18next.t("organization:All")} text={"organization:All"} >
<div style={{display: "flex", justifyContent: "space-between", marginRight: "10px"}}>
{i18next.t("organization:All")}
</div>
</Option>) : null
}
{ {
Setting.getCountryCodeData(countryCodes).map((country) => Setting.getCountryCodeOption(country)) Setting.getCountryCodeData(countryCodes).map((country) => Setting.getCountryCodeOption(country))
} }

View File

@ -80,6 +80,7 @@
"Only signup": "Only signup", "Only signup": "Only signup",
"Org choice mode": "Org choice mode", "Org choice mode": "Org choice mode",
"Org choice mode - Tooltip": "Org choice mode - Tooltip", "Org choice mode - Tooltip": "Org choice mode - Tooltip",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"",
"Please input your application!": "Please input your application!", "Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!", "Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file", "Please select a HTML file": "Please select a HTML file",
@ -165,7 +166,7 @@
"Next Step": "Next Step", "Next Step": "Next Step",
"Please input your username!": "Please input your username!", "Please input your username!": "Please input your username!",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "Retrieve password", "Reset password": "Reset password",
"Unknown forget type": "Unknown forget type", "Unknown forget type": "Unknown forget type",
"Verify": "Verify" "Verify": "Verify"
}, },
@ -229,6 +230,7 @@
"Email": "Email", "Email": "Email",
"Email - Tooltip": "Valid email address", "Email - Tooltip": "Valid email address",
"Email only": "Email only", "Email only": "Email only",
"Email or Phone": "Email or Phone",
"Enable": "Enable", "Enable": "Enable",
"Enable dark logo": "Enable dark logo", "Enable dark logo": "Enable dark logo",
"Enable dark logo - Tooltip": "Enable dark logo", "Enable dark logo - Tooltip": "Enable dark logo",
@ -311,6 +313,7 @@
"Phone": "Phone", "Phone": "Phone",
"Phone - Tooltip": "Phone number", "Phone - Tooltip": "Phone number",
"Phone only": "Phone only", "Phone only": "Phone only",
"Phone or Email": "Phone or Email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Plans", "Plans": "Plans",
@ -391,6 +394,7 @@
"User type": "User type", "User type": "User type",
"User type - Tooltip": "Tags that the user belongs to, defaulting to \"normal-user\"", "User type - Tooltip": "Tags that the user belongs to, defaulting to \"normal-user\"",
"Users": "Users", "Users": "Users",
"Users - Tooltip": "Users - Tooltip",
"Users under all organizations": "Users under all organizations", "Users under all organizations": "Users under all organizations",
"Verifications": "Verifications", "Verifications": "Verifications",
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
@ -473,7 +477,6 @@
"LDAP username, Email or phone": "LDAP username, Email or phone", "LDAP username, Email or phone": "LDAP username, Email or phone",
"Loading": "Loading", "Loading": "Loading",
"Logging out...": "Logging out...", "Logging out...": "Logging out...",
"Login button": "Login button",
"MetaMask plugin not detected": "MetaMask plugin not detected", "MetaMask plugin not detected": "MetaMask plugin not detected",
"Model loading failure": "Model loading failure", "Model loading failure": "Model loading failure",
"No account?": "No account?", "No account?": "No account?",
@ -498,6 +501,7 @@
"Sign in with Face ID": "Sign in with Face ID", "Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Sign in with {type}", "Sign in with {type}": "Sign in with {type}",
"Signin button": "Signin button",
"Signing in...": "Signing in...", "Signing in...": "Signing in...",
"Successfully logged in with WebAuthn credentials": "Successfully logged in with WebAuthn credentials", "Successfully logged in with WebAuthn credentials": "Successfully logged in with WebAuthn credentials",
"The camera is currently in use by another webpage": "The camera is currently in use by another webpage", "The camera is currently in use by another webpage": "The camera is currently in use by another webpage",
@ -572,6 +576,8 @@
"Soft deletion - Tooltip": "When enabled, deleting users will not completely remove them from the database. Instead, they will be marked as deleted", "Soft deletion - Tooltip": "When enabled, deleting users will not completely remove them from the database. Instead, they will be marked as deleted",
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Collection of tags available for users to choose from", "Tags - Tooltip": "Collection of tags available for users to choose from",
"Use Email as username": "Use Email as username",
"Use Email as username - Tooltip": "Use Email as username if the username field is not visible at signup",
"View rule": "View rule", "View rule": "View rule",
"Visible": "Visible", "Visible": "Visible",
"Website URL": "Website URL", "Website URL": "Website URL",
@ -615,6 +621,7 @@
"Processing...": "Processing...", "Processing...": "Processing...",
"Product": "Product", "Product": "Product",
"Product - Tooltip": "Product Name", "Product - Tooltip": "Product Name",
"Recharged successfully": "Recharged successfully",
"Result": "Result", "Result": "Result",
"Return to Website": "Return to Website", "Return to Website": "Return to Website",
"The payment has been canceled": "The payment has been canceled", "The payment has been canceled": "The payment has been canceled",
@ -623,6 +630,8 @@
"The payment is still under processing": "The payment is still under processing", "The payment is still under processing": "The payment is still under processing",
"Type - Tooltip": "Payment method used when purchasing the product", "Type - Tooltip": "Payment method used when purchasing the product",
"You have successfully completed the payment": "You have successfully completed the payment", "You have successfully completed the payment": "You have successfully completed the payment",
"You have successfully recharged": "You have successfully recharged",
"Your current balance is": "Your current balance is",
"please wait for a few seconds...": "please wait for a few seconds...", "please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is" "the current state is": "the current state is"
}, },
@ -685,6 +694,8 @@
"Edit Product": "Edit Product", "Edit Product": "Edit Product",
"Image": "Image", "Image": "Image",
"Image - Tooltip": "Image of product", "Image - Tooltip": "Image of product",
"Is recharge": "Is recharge",
"Is recharge - Tooltip": "Whether the current product is to recharge balance",
"New Product": "New Product", "New Product": "New Product",
"Pay": "Pay", "Pay": "Pay",
"PayPal": "PayPal", "PayPal": "PayPal",
@ -774,6 +785,8 @@
"From address - Tooltip": "Email address of \"From\"", "From address - Tooltip": "Email address of \"From\"",
"From name": "From name", "From name": "From name",
"From name - Tooltip": "Name of \"From\"", "From name - Tooltip": "Name of \"From\"",
"Get phone number": "Get phone number",
"Get phone number - Tooltip": "If sync phone number is enabled, you should enable google people api first and add scope https://www.googleapis.com/auth/user.phonenumbers.read",
"Host": "Host", "Host": "Host",
"Host - Tooltip": "Name of host", "Host - Tooltip": "Name of host",
"IdP": "IdP", "IdP": "IdP",
@ -893,7 +906,8 @@
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object", "Object": "Object",
"Response": "Response" "Response": "Response",
"Status code": "Status code"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",
@ -941,6 +955,7 @@
"Please select your country code!": "Please select your country code!", "Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Please select your country/region!", "Please select your country/region!": "Please select your country/region!",
"Regex": "Regex", "Regex": "Regex",
"Signup button": "Signup button",
"Terms of Use": "Terms of Use", "Terms of Use": "Terms of Use",
"Terms of Use - Tooltip": "Terms of use that users need to read and agree to during registration", "Terms of Use - Tooltip": "Terms of use that users need to read and agree to during registration",
"Text 1": "Text 1", "Text 1": "Text 1",
@ -948,6 +963,7 @@
"Text 3": "Text 3", "Text 3": "Text 3",
"Text 4": "Text 4", "Text 4": "Text 4",
"Text 5": "Text 5", "Text 5": "Text 5",
"The input Email doesn't match the signup item regex!": "The input Email doesn't match the signup item regex!",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!", "The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
"The input is not invoice title!": "The input is not invoice title!", "The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "The input is not valid Email!", "The input is not valid Email!": "The input is not valid Email!",
@ -1068,8 +1084,11 @@
"3rd-party logins - Tooltip": "Social logins linked by the user", "3rd-party logins - Tooltip": "Social logins linked by the user",
"Address": "Address", "Address": "Address",
"Address - Tooltip": "Residential address", "Address - Tooltip": "Residential address",
"Address line": "Address line",
"Affiliation": "Affiliation", "Affiliation": "Affiliation",
"Affiliation - Tooltip": "Employer, such as company name or organization name", "Affiliation - Tooltip": "Employer, such as company name or organization name",
"Balance": "Balance",
"Balance - Tooltip": "User's balance",
"Bio": "Bio", "Bio": "Bio",
"Bio - Tooltip": "Self introduction of the user", "Bio - Tooltip": "Self introduction of the user",
"Birthday": "Birthday", "Birthday": "Birthday",
@ -1120,6 +1139,8 @@
"Managed accounts": "Managed accounts", "Managed accounts": "Managed accounts",
"Modify password...": "Modify password...", "Modify password...": "Modify password...",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Multi-factor authentication",
"Need update password": "Need update password",
"Need update password - Tooltip": "Force user update password after login",
"New Email": "New Email", "New Email": "New Email",
"New Password": "New Password", "New Password": "New Password",
"New User": "New User", "New User": "New User",
@ -1162,9 +1183,11 @@
"Values": "Values", "Values": "Values",
"Verification code sent": "Verification code sent", "Verification code sent": "Verification code sent",
"WebAuthn credentials": "WebAuthn credentials", "WebAuthn credentials": "WebAuthn credentials",
"You have changed the username, please save your change first before modifying the password": "You have changed the username, please save your change first before modifying the password",
"input password": "input password" "input password": "input password"
}, },
"verification": { "verification": {
"Is used": "Is used",
"Receiver": "Receiver" "Receiver": "Receiver"
}, },
"webhook": { "webhook": {

View File

@ -80,6 +80,7 @@
"Only signup": "Only signup", "Only signup": "Only signup",
"Org choice mode": "Org choice mode", "Org choice mode": "Org choice mode",
"Org choice mode - Tooltip": "Org choice mode - Tooltip", "Org choice mode - Tooltip": "Org choice mode - Tooltip",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"",
"Please input your application!": "Bitte geben Sie Ihre Anwendung ein!", "Please input your application!": "Bitte geben Sie Ihre Anwendung ein!",
"Please input your organization!": "Bitte geben Sie Ihre Organisation ein!", "Please input your organization!": "Bitte geben Sie Ihre Organisation ein!",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei aus", "Please select a HTML file": "Bitte wählen Sie eine HTML-Datei aus",
@ -165,7 +166,7 @@
"Next Step": "Nächster Schritt", "Next Step": "Nächster Schritt",
"Please input your username!": "Bitte gib deinen Benutzernamen ein!", "Please input your username!": "Bitte gib deinen Benutzernamen ein!",
"Reset": "Zurücksetzen", "Reset": "Zurücksetzen",
"Retrieve password": "Passwort abrufen", "Reset password": "Passwort abrufen",
"Unknown forget type": "Unbekannter Vergesslichkeitstyp", "Unknown forget type": "Unbekannter Vergesslichkeitstyp",
"Verify": "überprüfen" "Verify": "überprüfen"
}, },
@ -229,6 +230,7 @@
"Email": "E-Mail", "Email": "E-Mail",
"Email - Tooltip": "Gültige E-Mail-Adresse", "Email - Tooltip": "Gültige E-Mail-Adresse",
"Email only": "Email only", "Email only": "Email only",
"Email or Phone": "Email or Phone",
"Enable": "Enable", "Enable": "Enable",
"Enable dark logo": "Enable dark logo", "Enable dark logo": "Enable dark logo",
"Enable dark logo - Tooltip": "Enable dark logo", "Enable dark logo - Tooltip": "Enable dark logo",
@ -311,6 +313,7 @@
"Phone": "Telefon", "Phone": "Telefon",
"Phone - Tooltip": "Telefonnummer", "Phone - Tooltip": "Telefonnummer",
"Phone only": "Phone only", "Phone only": "Phone only",
"Phone or Email": "Phone or Email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Pläne", "Plans": "Pläne",
@ -391,6 +394,7 @@
"User type": "Benutzertyp", "User type": "Benutzertyp",
"User type - Tooltip": "Tags, denen der Benutzer angehört, standardmäßig auf \"normaler Benutzer\" festgelegt", "User type - Tooltip": "Tags, denen der Benutzer angehört, standardmäßig auf \"normaler Benutzer\" festgelegt",
"Users": "Benutzer", "Users": "Benutzer",
"Users - Tooltip": "Users - Tooltip",
"Users under all organizations": "Benutzer unter allen Organisationen", "Users under all organizations": "Benutzer unter allen Organisationen",
"Verifications": "Verifications", "Verifications": "Verifications",
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
@ -473,7 +477,6 @@
"LDAP username, Email or phone": "LDAP username, Email or phone", "LDAP username, Email or phone": "LDAP username, Email or phone",
"Loading": "Laden", "Loading": "Laden",
"Logging out...": "Ausloggen...", "Logging out...": "Ausloggen...",
"Login button": "Login button",
"MetaMask plugin not detected": "MetaMask plugin not detected", "MetaMask plugin not detected": "MetaMask plugin not detected",
"Model loading failure": "Model loading failure", "Model loading failure": "Model loading failure",
"No account?": "Kein Konto?", "No account?": "Kein Konto?",
@ -498,6 +501,7 @@
"Sign in with Face ID": "Sign in with Face ID", "Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Melden Sie sich mit WebAuthn an", "Sign in with WebAuthn": "Melden Sie sich mit WebAuthn an",
"Sign in with {type}": "Melden Sie sich mit {type} an", "Sign in with {type}": "Melden Sie sich mit {type} an",
"Signin button": "Signin button",
"Signing in...": "Anmelden...", "Signing in...": "Anmelden...",
"Successfully logged in with WebAuthn credentials": "Erfolgreich mit WebAuthn-Anmeldeinformationen angemeldet", "Successfully logged in with WebAuthn credentials": "Erfolgreich mit WebAuthn-Anmeldeinformationen angemeldet",
"The camera is currently in use by another webpage": "The camera is currently in use by another webpage", "The camera is currently in use by another webpage": "The camera is currently in use by another webpage",
@ -572,6 +576,8 @@
"Soft deletion - Tooltip": "Wenn aktiviert, werden gelöschte Benutzer nicht vollständig aus der Datenbank entfernt. Stattdessen werden sie als gelöscht markiert", "Soft deletion - Tooltip": "Wenn aktiviert, werden gelöschte Benutzer nicht vollständig aus der Datenbank entfernt. Stattdessen werden sie als gelöscht markiert",
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Sammlung von Tags, die für Benutzer zur Auswahl zur Verfügung stehen", "Tags - Tooltip": "Sammlung von Tags, die für Benutzer zur Auswahl zur Verfügung stehen",
"Use Email as username": "Use Email as username",
"Use Email as username - Tooltip": "Use Email as username if the username field is not visible at signup",
"View rule": "Ansichtsregel", "View rule": "Ansichtsregel",
"Visible": "Sichtbar", "Visible": "Sichtbar",
"Website URL": "Website-URL", "Website URL": "Website-URL",
@ -615,6 +621,7 @@
"Processing...": "In Bearbeitung...", "Processing...": "In Bearbeitung...",
"Product": "Produkt", "Product": "Produkt",
"Product - Tooltip": "Produktname", "Product - Tooltip": "Produktname",
"Recharged successfully": "Recharged successfully",
"Result": "Ergebnis", "Result": "Ergebnis",
"Return to Website": "Zurück zur Website", "Return to Website": "Zurück zur Website",
"The payment has been canceled": "The payment has been canceled", "The payment has been canceled": "The payment has been canceled",
@ -623,6 +630,8 @@
"The payment is still under processing": "Die Zahlung wird immer noch bearbeitet", "The payment is still under processing": "Die Zahlung wird immer noch bearbeitet",
"Type - Tooltip": "Zahlungsmethode, die beim Kauf des Produkts verwendet wurde", "Type - Tooltip": "Zahlungsmethode, die beim Kauf des Produkts verwendet wurde",
"You have successfully completed the payment": "Sie haben die Zahlung erfolgreich abgeschlossen", "You have successfully completed the payment": "Sie haben die Zahlung erfolgreich abgeschlossen",
"You have successfully recharged": "You have successfully recharged",
"Your current balance is": "Your current balance is",
"please wait for a few seconds...": "Bitte warten Sie ein paar Sekunden...", "please wait for a few seconds...": "Bitte warten Sie ein paar Sekunden...",
"the current state is": "der aktuelle Zustand ist" "the current state is": "der aktuelle Zustand ist"
}, },
@ -685,6 +694,8 @@
"Edit Product": "Produkt bearbeiten", "Edit Product": "Produkt bearbeiten",
"Image": "Bild", "Image": "Bild",
"Image - Tooltip": "Bild des Produkts", "Image - Tooltip": "Bild des Produkts",
"Is recharge": "Is recharge",
"Is recharge - Tooltip": "Whether the current product is to recharge balance",
"New Product": "Neues Produkt", "New Product": "Neues Produkt",
"Pay": "Zahlen", "Pay": "Zahlen",
"PayPal": "PayPal", "PayPal": "PayPal",
@ -774,6 +785,8 @@
"From address - Tooltip": "From address - Tooltip", "From address - Tooltip": "From address - Tooltip",
"From name": "From name", "From name": "From name",
"From name - Tooltip": "From name - Tooltip", "From name - Tooltip": "From name - Tooltip",
"Get phone number": "Get phone number",
"Get phone number - Tooltip": "If sync phone number is enabled, you should enable google people api first and add scope https://www.googleapis.com/auth/user.phonenumbers.read",
"Host": "Host", "Host": "Host",
"Host - Tooltip": "Name des Hosts", "Host - Tooltip": "Name des Hosts",
"IdP": "IdP", "IdP": "IdP",
@ -893,7 +906,8 @@
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object", "Object": "Object",
"Response": "Response" "Response": "Response",
"Status code": "Status code"
}, },
"resource": { "resource": {
"Copy Link": "Kopiere den Link", "Copy Link": "Kopiere den Link",
@ -941,6 +955,7 @@
"Please select your country code!": "Bitte wählen Sie Ihren Ländercode aus!", "Please select your country code!": "Bitte wählen Sie Ihren Ländercode aus!",
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region aus!", "Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region aus!",
"Regex": "Regex", "Regex": "Regex",
"Signup button": "Signup button",
"Terms of Use": "Nutzungsbedingungen", "Terms of Use": "Nutzungsbedingungen",
"Terms of Use - Tooltip": "Nutzungsbedingungen, die Benutzer während der Registrierung lesen und akzeptieren müssen", "Terms of Use - Tooltip": "Nutzungsbedingungen, die Benutzer während der Registrierung lesen und akzeptieren müssen",
"Text 1": "Text 1", "Text 1": "Text 1",
@ -948,6 +963,7 @@
"Text 3": "Text 3", "Text 3": "Text 3",
"Text 4": "Text 4", "Text 4": "Text 4",
"Text 5": "Text 5", "Text 5": "Text 5",
"The input Email doesn't match the signup item regex!": "The input Email doesn't match the signup item regex!",
"The input is not invoice Tax ID!": "Die Eingabe ist keine Rechnungssteuer-ID!", "The input is not invoice Tax ID!": "Die Eingabe ist keine Rechnungssteuer-ID!",
"The input is not invoice title!": "Der Eingabewert ist nicht die Rechnungsbezeichnung!", "The input is not invoice title!": "Der Eingabewert ist nicht die Rechnungsbezeichnung!",
"The input is not valid Email!": "Die Eingabe ist keine gültige E-Mail-Adresse!", "The input is not valid Email!": "Die Eingabe ist keine gültige E-Mail-Adresse!",
@ -1068,8 +1084,11 @@
"3rd-party logins - Tooltip": "Drittanbieter-Anmeldungen, die mit dem Benutzer verknüpft sind", "3rd-party logins - Tooltip": "Drittanbieter-Anmeldungen, die mit dem Benutzer verknüpft sind",
"Address": "Adresse", "Address": "Adresse",
"Address - Tooltip": "Wohnadresse", "Address - Tooltip": "Wohnadresse",
"Address line": "Address line",
"Affiliation": "Zugehörigkeit", "Affiliation": "Zugehörigkeit",
"Affiliation - Tooltip": "Arbeitgeber, wie Firmenname oder Organisationsname", "Affiliation - Tooltip": "Arbeitgeber, wie Firmenname oder Organisationsname",
"Balance": "Balance",
"Balance - Tooltip": "User's balance",
"Bio": "Bio", "Bio": "Bio",
"Bio - Tooltip": "Selbstvorstellung des Nutzers", "Bio - Tooltip": "Selbstvorstellung des Nutzers",
"Birthday": "Birthday", "Birthday": "Birthday",
@ -1120,6 +1139,8 @@
"Managed accounts": "Verwaltete Konten", "Managed accounts": "Verwaltete Konten",
"Modify password...": "Passwort ändern...", "Modify password...": "Passwort ändern...",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Multi-factor authentication",
"Need update password": "Need update password",
"Need update password - Tooltip": "Force user update password after login",
"New Email": "Neue E-Mail", "New Email": "Neue E-Mail",
"New Password": "Neues Passwort", "New Password": "Neues Passwort",
"New User": "Neuer Benutzer", "New User": "Neuer Benutzer",
@ -1162,9 +1183,11 @@
"Values": "Werte", "Values": "Werte",
"Verification code sent": "Bestätigungscode gesendet", "Verification code sent": "Bestätigungscode gesendet",
"WebAuthn credentials": "WebAuthn-Anmeldeinformationen", "WebAuthn credentials": "WebAuthn-Anmeldeinformationen",
"You have changed the username, please save your change first before modifying the password": "You have changed the username, please save your change first before modifying the password",
"input password": "Eingabe des Passworts" "input password": "Eingabe des Passworts"
}, },
"verification": { "verification": {
"Is used": "Is used",
"Receiver": "Receiver" "Receiver": "Receiver"
}, },
"webhook": { "webhook": {

View File

@ -80,6 +80,7 @@
"Only signup": "Only signup", "Only signup": "Only signup",
"Org choice mode": "Org choice mode", "Org choice mode": "Org choice mode",
"Org choice mode - Tooltip": "Org choice mode - Tooltip", "Org choice mode - Tooltip": "Org choice mode - Tooltip",
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"",
"Please input your application!": "Please input your application!", "Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!", "Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file", "Please select a HTML file": "Please select a HTML file",
@ -165,7 +166,7 @@
"Next Step": "Next Step", "Next Step": "Next Step",
"Please input your username!": "Please input your username!", "Please input your username!": "Please input your username!",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "Retrieve password", "Reset password": "Reset password",
"Unknown forget type": "Unknown forget type", "Unknown forget type": "Unknown forget type",
"Verify": "Verify" "Verify": "Verify"
}, },
@ -229,6 +230,7 @@
"Email": "Email", "Email": "Email",
"Email - Tooltip": "Valid email address", "Email - Tooltip": "Valid email address",
"Email only": "Email only", "Email only": "Email only",
"Email or Phone": "Email or Phone",
"Enable": "Enable", "Enable": "Enable",
"Enable dark logo": "Enable dark logo", "Enable dark logo": "Enable dark logo",
"Enable dark logo - Tooltip": "Enable dark logo", "Enable dark logo - Tooltip": "Enable dark logo",
@ -311,6 +313,7 @@
"Phone": "Phone", "Phone": "Phone",
"Phone - Tooltip": "Phone number", "Phone - Tooltip": "Phone number",
"Phone only": "Phone only", "Phone only": "Phone only",
"Phone or Email": "Phone or Email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Plans", "Plans": "Plans",
@ -391,6 +394,7 @@
"User type": "User type", "User type": "User type",
"User type - Tooltip": "Tags that the user belongs to, defaulting to \"normal-user\"", "User type - Tooltip": "Tags that the user belongs to, defaulting to \"normal-user\"",
"Users": "Users", "Users": "Users",
"Users - Tooltip": "Users - Tooltip",
"Users under all organizations": "Users under all organizations", "Users under all organizations": "Users under all organizations",
"Verifications": "Verifications", "Verifications": "Verifications",
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
@ -473,7 +477,6 @@
"LDAP username, Email or phone": "LDAP username, Email or phone", "LDAP username, Email or phone": "LDAP username, Email or phone",
"Loading": "Loading", "Loading": "Loading",
"Logging out...": "Logging out...", "Logging out...": "Logging out...",
"Login button": "Login button",
"MetaMask plugin not detected": "MetaMask plugin not detected", "MetaMask plugin not detected": "MetaMask plugin not detected",
"Model loading failure": "Model loading failure", "Model loading failure": "Model loading failure",
"No account?": "No account?", "No account?": "No account?",
@ -498,6 +501,7 @@
"Sign in with Face ID": "Sign in with Face ID", "Sign in with Face ID": "Sign in with Face ID",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Sign in with {type}", "Sign in with {type}": "Sign in with {type}",
"Signin button": "Signin button",
"Signing in...": "Signing in...", "Signing in...": "Signing in...",
"Successfully logged in with WebAuthn credentials": "Successfully logged in with WebAuthn credentials", "Successfully logged in with WebAuthn credentials": "Successfully logged in with WebAuthn credentials",
"The camera is currently in use by another webpage": "The camera is currently in use by another webpage", "The camera is currently in use by another webpage": "The camera is currently in use by another webpage",
@ -572,6 +576,8 @@
"Soft deletion - Tooltip": "When enabled, deleting users will not completely remove them from the database. Instead, they will be marked as deleted", "Soft deletion - Tooltip": "When enabled, deleting users will not completely remove them from the database. Instead, they will be marked as deleted",
"Tags": "Tags", "Tags": "Tags",
"Tags - Tooltip": "Collection of tags available for users to choose from", "Tags - Tooltip": "Collection of tags available for users to choose from",
"Use Email as username": "Use Email as username",
"Use Email as username - Tooltip": "Use Email as username if the username field is not visible at signup",
"View rule": "View rule", "View rule": "View rule",
"Visible": "Visible", "Visible": "Visible",
"Website URL": "Website URL", "Website URL": "Website URL",
@ -615,6 +621,7 @@
"Processing...": "Processing...", "Processing...": "Processing...",
"Product": "Product", "Product": "Product",
"Product - Tooltip": "Product Name", "Product - Tooltip": "Product Name",
"Recharged successfully": "Recharged successfully",
"Result": "Result", "Result": "Result",
"Return to Website": "Return to Website", "Return to Website": "Return to Website",
"The payment has been canceled": "The payment has been canceled", "The payment has been canceled": "The payment has been canceled",
@ -623,6 +630,8 @@
"The payment is still under processing": "The payment is still under processing", "The payment is still under processing": "The payment is still under processing",
"Type - Tooltip": "Payment method used when purchasing the product", "Type - Tooltip": "Payment method used when purchasing the product",
"You have successfully completed the payment": "You have successfully completed the payment", "You have successfully completed the payment": "You have successfully completed the payment",
"You have successfully recharged": "You have successfully recharged",
"Your current balance is": "Your current balance is",
"please wait for a few seconds...": "please wait for a few seconds...", "please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is" "the current state is": "the current state is"
}, },
@ -685,6 +694,8 @@
"Edit Product": "Edit Product", "Edit Product": "Edit Product",
"Image": "Image", "Image": "Image",
"Image - Tooltip": "Image of product", "Image - Tooltip": "Image of product",
"Is recharge": "Is recharge",
"Is recharge - Tooltip": "Whether the current product is to recharge balance",
"New Product": "New Product", "New Product": "New Product",
"Pay": "Pay", "Pay": "Pay",
"PayPal": "PayPal", "PayPal": "PayPal",
@ -774,6 +785,8 @@
"From address - Tooltip": "Email address of \"From\"", "From address - Tooltip": "Email address of \"From\"",
"From name": "From name", "From name": "From name",
"From name - Tooltip": "Name of \"From\"", "From name - Tooltip": "Name of \"From\"",
"Get phone number": "Get phone number",
"Get phone number - Tooltip": "If sync phone number is enabled, you should enable google people api first and add scope https://www.googleapis.com/auth/user.phonenumbers.read",
"Host": "Host", "Host": "Host",
"Host - Tooltip": "Name of host", "Host - Tooltip": "Name of host",
"IdP": "IdP", "IdP": "IdP",
@ -893,7 +906,8 @@
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object", "Object": "Object",
"Response": "Response" "Response": "Response",
"Status code": "Status code"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",
@ -941,6 +955,7 @@
"Please select your country code!": "Please select your country code!", "Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Please select your country/region!", "Please select your country/region!": "Please select your country/region!",
"Regex": "Regex", "Regex": "Regex",
"Signup button": "Signup button",
"Terms of Use": "Terms of Use", "Terms of Use": "Terms of Use",
"Terms of Use - Tooltip": "Terms of use that users need to read and agree to during registration", "Terms of Use - Tooltip": "Terms of use that users need to read and agree to during registration",
"Text 1": "Text 1", "Text 1": "Text 1",
@ -948,6 +963,7 @@
"Text 3": "Text 3", "Text 3": "Text 3",
"Text 4": "Text 4", "Text 4": "Text 4",
"Text 5": "Text 5", "Text 5": "Text 5",
"The input Email doesn't match the signup item regex!": "The input Email doesn't match the signup item regex!",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!", "The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
"The input is not invoice title!": "The input is not invoice title!", "The input is not invoice title!": "The input is not invoice title!",
"The input is not valid Email!": "The input is not valid Email!", "The input is not valid Email!": "The input is not valid Email!",
@ -1068,8 +1084,11 @@
"3rd-party logins - Tooltip": "Social logins linked by the user", "3rd-party logins - Tooltip": "Social logins linked by the user",
"Address": "Address", "Address": "Address",
"Address - Tooltip": "Residential address", "Address - Tooltip": "Residential address",
"Address line": "Address line",
"Affiliation": "Affiliation", "Affiliation": "Affiliation",
"Affiliation - Tooltip": "Employer, such as company name or organization name", "Affiliation - Tooltip": "Employer, such as company name or organization name",
"Balance": "Balance",
"Balance - Tooltip": "User's balance",
"Bio": "Bio", "Bio": "Bio",
"Bio - Tooltip": "Self introduction of the user", "Bio - Tooltip": "Self introduction of the user",
"Birthday": "Birthday", "Birthday": "Birthday",
@ -1120,6 +1139,8 @@
"Managed accounts": "Managed accounts", "Managed accounts": "Managed accounts",
"Modify password...": "Modify password...", "Modify password...": "Modify password...",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Multi-factor authentication",
"Need update password": "Need update password",
"Need update password - Tooltip": "Force user update password after login",
"New Email": "New Email", "New Email": "New Email",
"New Password": "New Password", "New Password": "New Password",
"New User": "New User", "New User": "New User",
@ -1162,9 +1183,11 @@
"Values": "Values", "Values": "Values",
"Verification code sent": "Verification code sent", "Verification code sent": "Verification code sent",
"WebAuthn credentials": "WebAuthn credentials", "WebAuthn credentials": "WebAuthn credentials",
"You have changed the username, please save your change first before modifying the password": "You have changed the username, please save your change first before modifying the password",
"input password": "input password" "input password": "input password"
}, },
"verification": { "verification": {
"Is used": "Is used",
"Receiver": "Receiver" "Receiver": "Receiver"
}, },
"webhook": { "webhook": {

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