forked from casdoor/casdoor
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0748661d2a | ||
|
|
83552ed143 | ||
|
|
8cb8541f96 | ||
|
|
5b646a726c | ||
|
|
19b9586670 | ||
|
|
73f8d19c5f | ||
|
|
04da531df3 | ||
|
|
d97558051d | ||
|
|
ac55355290 | ||
|
|
a2da380be4 | ||
|
|
ecf8039c5d | ||
|
|
0a6948034c | ||
|
|
442f8fb19e | ||
|
|
b771add9e3 | ||
|
|
df8e9fceea | ||
|
|
d674f0c33d | ||
|
|
1e1b5273d9 | ||
|
|
cf5e88915c | ||
|
|
c8973e6c9e | ||
|
|
87ea451561 | ||
|
|
8f32779b42 | ||
|
|
aba471b4e8 | ||
|
|
72b70c3b03 | ||
|
|
a1c56894c7 | ||
|
|
a9ae9394c7 | ||
|
|
5f0fa5f23e | ||
|
|
f99aa047a9 |
12
Dockerfile
12
Dockerfile
@@ -51,22 +51,14 @@ COPY --from=FRONT --chown=$USER:$USER /web/build ./web/build
|
||||
ENTRYPOINT ["/server"]
|
||||
|
||||
|
||||
FROM debian:latest AS db
|
||||
RUN apt update \
|
||||
&& apt install -y \
|
||||
mariadb-server \
|
||||
mariadb-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
FROM db AS ALLINONE
|
||||
FROM debian:latest AS ALLINONE
|
||||
LABEL MAINTAINER="https://casdoor.org/"
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y ca-certificates && update-ca-certificates
|
||||
RUN apt install -y ca-certificates lsof && update-ca-certificates
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server
|
||||
|
||||
@@ -312,6 +312,29 @@ func (c *ApiController) Signup() {
|
||||
userId := user.GetId()
|
||||
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
|
||||
|
||||
// Check if this is an OAuth flow and automatically generate code
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
redirectUri := c.Ctx.Input.Query("redirectUri")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
state := c.Ctx.Input.Query("state")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
codeChallenge := c.Ctx.Input.Query("code_challenge")
|
||||
|
||||
// If OAuth parameters are present, generate OAuth code and return it
|
||||
if clientId != "" && responseType == ResponseTypeCode {
|
||||
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp := codeToResponse(code)
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(userId)
|
||||
}
|
||||
|
||||
|
||||
@@ -867,10 +867,16 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsSignupItemRequired("Invitation code") {
|
||||
c.ResponseError(c.T("check:Invitation code cannot be blank"))
|
||||
// Check and validate invitation code
|
||||
invitation, msg := object.CheckInvitationCode(application, organization, &authForm, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
invitationName := ""
|
||||
if invitation != nil {
|
||||
invitationName = invitation.Name
|
||||
}
|
||||
|
||||
// Handle UseEmailAsUsername for OAuth and Web3
|
||||
if organization.UseEmailAsUsername && userInfo.Email != "" {
|
||||
@@ -937,11 +943,16 @@ func (c *ApiController) Login() {
|
||||
IsDeleted: false,
|
||||
SignupApplication: application.Name,
|
||||
Properties: properties,
|
||||
Invitation: invitationName,
|
||||
InvitationCode: authForm.InvitationCode,
|
||||
RegisterType: "Application Signup",
|
||||
RegisterSource: fmt.Sprintf("%s/%s", application.Organization, application.Name),
|
||||
}
|
||||
|
||||
if providerItem.SignupGroup != "" {
|
||||
// Set group from invitation code if available, otherwise use provider's signup group
|
||||
if invitation != nil && invitation.SignupGroup != "" {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
} else if providerItem.SignupGroup != "" {
|
||||
user.Groups = []string{providerItem.SignupGroup}
|
||||
}
|
||||
|
||||
@@ -956,6 +967,16 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to create user, user information is invalid: %s"), util.StructToJson(user)))
|
||||
return
|
||||
}
|
||||
|
||||
// Increment invitation usage count
|
||||
if invitation != nil {
|
||||
invitation.UsedCount += 1
|
||||
_, err = object.UpdateInvitation(invitation.GetId(), invitation, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sync info from 3rd-party if possible
|
||||
|
||||
@@ -303,6 +303,13 @@ func (c *ApiController) BatchEnforce() {
|
||||
c.ResponseOk(res, keyRes)
|
||||
}
|
||||
|
||||
// GetAllObjects
|
||||
// @Title GetAllObjects
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all objects for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-objects [get]
|
||||
func (c *ApiController) GetAllObjects() {
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
@@ -322,6 +329,13 @@ func (c *ApiController) GetAllObjects() {
|
||||
c.ResponseOk(objects)
|
||||
}
|
||||
|
||||
// GetAllActions
|
||||
// @Title GetAllActions
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all actions for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-actions [get]
|
||||
func (c *ApiController) GetAllActions() {
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
@@ -341,6 +355,13 @@ func (c *ApiController) GetAllActions() {
|
||||
c.ResponseOk(actions)
|
||||
}
|
||||
|
||||
// GetAllRoles
|
||||
// @Title GetAllRoles
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all roles for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-roles [get]
|
||||
func (c *ApiController) GetAllRoles() {
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *ApiController) GetInvitationCodeInfo() {
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The application: %s does not exist"), applicationId))
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationId))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func (c *ApiController) SendInvitation() {
|
||||
return
|
||||
}
|
||||
if organization == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The organization: %s does not exist"), invitation.Owner))
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s does not exist"), invitation.Owner))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -150,6 +151,26 @@ func (c *ApiController) AddSubscription() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if plan restricts user to one subscription
|
||||
if subscription.Plan != "" {
|
||||
plan, err := object.GetPlan(util.GetId(subscription.Owner, subscription.Plan))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if plan != nil && plan.IsExclusive {
|
||||
hasSubscription, err := object.HasActiveSubscriptionForPlan(subscription.Owner, subscription.User, subscription.Plan)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if hasSubscription {
|
||||
c.ResponseError(fmt.Sprintf("User already has an active subscription for plan: %s", subscription.Plan))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddSubscription(&subscription))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -173,6 +173,9 @@ func (c *ApiController) GetOAuthToken() {
|
||||
avatar := c.Ctx.Input.Query("avatar")
|
||||
refreshToken := c.Ctx.Input.Query("refresh_token")
|
||||
deviceCode := c.Ctx.Input.Query("device_code")
|
||||
subjectToken := c.Ctx.Input.Query("subject_token")
|
||||
subjectTokenType := c.Ctx.Input.Query("subject_token_type")
|
||||
audience := c.Ctx.Input.Query("audience")
|
||||
|
||||
if clientId == "" && clientSecret == "" {
|
||||
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||
@@ -219,6 +222,15 @@ func (c *ApiController) GetOAuthToken() {
|
||||
if refreshToken == "" {
|
||||
refreshToken = tokenRequest.RefreshToken
|
||||
}
|
||||
if subjectToken == "" {
|
||||
subjectToken = tokenRequest.SubjectToken
|
||||
}
|
||||
if subjectTokenType == "" {
|
||||
subjectTokenType = tokenRequest.SubjectTokenType
|
||||
}
|
||||
if audience == "" {
|
||||
audience = tokenRequest.Audience
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +275,7 @@ func (c *ApiController) GetOAuthToken() {
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -15,16 +15,19 @@
|
||||
package controllers
|
||||
|
||||
type TokenRequest struct {
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
SubjectToken string `json:"subject_token"`
|
||||
SubjectTokenType string `json:"subject_token_type"`
|
||||
Audience string `json:"audience"`
|
||||
}
|
||||
|
||||
@@ -942,7 +942,7 @@ func (c *ApiController) VerifyIdentification() {
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("provider:The provider: %s does not exist"), providerName))
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), providerName))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,22 @@ func (c *ApiController) SendVerificationCode() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if "Forgot password?" signin item is visible when using forget verification
|
||||
if vform.Method == ForgetVerification {
|
||||
isForgotPasswordEnabled := false
|
||||
for _, item := range application.SigninItems {
|
||||
if item.Name == "Forgot password?" {
|
||||
isForgotPasswordEnabled = item.Visible
|
||||
break
|
||||
}
|
||||
}
|
||||
// Block access if the signin item is not found or is explicitly hidden
|
||||
if !isForgotPasswordEnabled {
|
||||
c.ResponseError(c.T("verification:The forgot password feature is disabled"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
organization, err := object.GetOrganization(util.GetId(application.Owner, application.Organization))
|
||||
if err != nil {
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/bin/bash
|
||||
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ;fi
|
||||
|
||||
service mariadb start
|
||||
if [ -z "${driverName:-}" ]; then
|
||||
export driverName=sqlite
|
||||
fi
|
||||
if [ -z "${dataSourceName:-}" ]; then
|
||||
export dataSourceName="file:casdoor.db?cache=shared"
|
||||
fi
|
||||
|
||||
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
|
||||
|
||||
exec /server --createDatabase=true
|
||||
exec /server
|
||||
|
||||
2
go.mod
2
go.mod
@@ -14,6 +14,7 @@ require (
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0
|
||||
github.com/alibabacloud-go/tea v1.3.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
|
||||
github.com/aliyun/credentials-go v1.3.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
@@ -111,7 +112,6 @@ require (
|
||||
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.45.5 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -766,8 +766,8 @@ github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCE
|
||||
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 h1:0LfzeUr4quwrrrTHn1kfLA0FBdsChCMs8eK2EzOwXVQ=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible h1:9gWa46nstkJ9miBReJcN8Gq34cBFbzSpQZVVT9N09TM=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
@@ -1294,7 +1294,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aW
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
|
||||
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
@@ -1308,7 +1307,6 @@ github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUB
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -2615,7 +2613,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
@@ -2637,6 +2634,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
140
i18n/deduplicate_test.go
Normal file
140
i18n/deduplicate_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2026 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 i18n
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// DuplicateInfo represents information about a duplicate key
|
||||
type DuplicateInfo struct {
|
||||
Key string
|
||||
OldPrefix string
|
||||
NewPrefix string
|
||||
OldPrefixKey string // e.g., "general:Submitter"
|
||||
NewPrefixKey string // e.g., "permission:Submitter"
|
||||
}
|
||||
|
||||
// findDuplicateKeysInJSON finds duplicate keys across the entire JSON file
|
||||
// Returns a list of duplicate information showing old and new prefix:key pairs
|
||||
// The order is determined by the order keys appear in the JSON file (git history)
|
||||
func findDuplicateKeysInJSON(filePath string) ([]DuplicateInfo, error) {
|
||||
// Read the JSON file
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Track the first occurrence of each key (prefix where it was first seen)
|
||||
keyFirstPrefix := make(map[string]string)
|
||||
var duplicates []DuplicateInfo
|
||||
|
||||
// To preserve order, we need to parse the JSON with order preservation
|
||||
// We'll use a decoder to read through the top-level object
|
||||
decoder := json.NewDecoder(bytes.NewReader(fileContent))
|
||||
|
||||
// Read the opening brace of the top-level object
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read token: %w", err)
|
||||
}
|
||||
if delim, ok := token.(json.Delim); !ok || delim != '{' {
|
||||
return nil, fmt.Errorf("expected object start, got %v", token)
|
||||
}
|
||||
|
||||
// Read all namespaces in order
|
||||
for decoder.More() {
|
||||
// Read the namespace (prefix) name
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read namespace: %w", err)
|
||||
}
|
||||
|
||||
prefix, ok := token.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected string namespace, got %v", token)
|
||||
}
|
||||
|
||||
// Read the namespace object as raw message
|
||||
var namespaceData map[string]string
|
||||
if err := decoder.Decode(&namespaceData); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode namespace %s: %w", prefix, err)
|
||||
}
|
||||
|
||||
// Now check each key in this namespace
|
||||
for key := range namespaceData {
|
||||
// Check if this key was already seen in a different prefix
|
||||
if firstPrefix, exists := keyFirstPrefix[key]; exists {
|
||||
// This is a duplicate - the key exists in another prefix
|
||||
duplicates = append(duplicates, DuplicateInfo{
|
||||
Key: key,
|
||||
OldPrefix: firstPrefix,
|
||||
NewPrefix: prefix,
|
||||
OldPrefixKey: fmt.Sprintf("%s:%s", firstPrefix, key),
|
||||
NewPrefixKey: fmt.Sprintf("%s:%s", prefix, key),
|
||||
})
|
||||
} else {
|
||||
// First time seeing this key, record the prefix
|
||||
keyFirstPrefix[key] = prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates, nil
|
||||
}
|
||||
|
||||
// TestDeduplicateFrontendI18n checks for duplicate i18n keys in the frontend en.json file
|
||||
func TestDeduplicateFrontendI18n(t *testing.T) {
|
||||
filePath := "../web/src/locales/en/data.json"
|
||||
|
||||
// Find duplicate keys
|
||||
duplicates, err := findDuplicateKeysInJSON(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check for duplicates in frontend i18n file: %v", err)
|
||||
}
|
||||
|
||||
// Print all duplicates and fail the test if any are found
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("Found duplicate i18n keys in frontend file (%s):", filePath)
|
||||
for _, dup := range duplicates {
|
||||
t.Errorf(" i18next.t(\"%s\") duplicates with i18next.t(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
|
||||
}
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeduplicateBackendI18n checks for duplicate i18n keys in the backend en.json file
|
||||
func TestDeduplicateBackendI18n(t *testing.T) {
|
||||
filePath := "../i18n/locales/en/data.json"
|
||||
|
||||
// Find duplicate keys
|
||||
duplicates, err := findDuplicateKeysInJSON(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check for duplicates in backend i18n file: %v", err)
|
||||
}
|
||||
|
||||
// Print all duplicates and fail the test if any are found
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("Found duplicate i18n keys in backend file (%s):", filePath)
|
||||
for _, dup := range duplicates {
|
||||
t.Errorf(" i18n.Translate(\"%s\") duplicates with i18n.Translate(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
|
||||
}
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Konnte den Benutzer nicht hinzufügen",
|
||||
"Get init score failed, error: %w": "Init-Score konnte nicht abgerufen werden, Fehler: %w",
|
||||
"Please sign out first": "Bitte melden Sie sich zuerst ab",
|
||||
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, sich für ein neues Konto anzumelden"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "Die Anmeldemethode: Anmeldung per E-Mail ist für die Anwendung nicht aktiviert",
|
||||
"The login method: login with face is not enabled for the application": "Die Anmeldemethode: Anmeldung per Gesicht ist für die Anwendung nicht aktiviert",
|
||||
"The login method: login with password is not enabled for the application": "Die Anmeldeart \"Anmeldung mit Passwort\" ist für die Anwendung nicht aktiviert",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "Die Organisation: %s existiert nicht",
|
||||
"The organization: %s has disabled users to signin": "Die Organisation: %s hat die Anmeldung von Benutzern deaktiviert",
|
||||
"The plan: %s does not exist": "Der Plan: %s existiert nicht",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Gesichtsdaten stimmen nicht überein",
|
||||
"Failed to parse client IP: %s": "Fehler beim Parsen der Client-IP: %s",
|
||||
"FirstName cannot be blank": "Vorname darf nicht leer sein",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "Einladungscode darf nicht leer sein",
|
||||
"Invitation code exhausted": "Einladungscode aufgebraucht",
|
||||
"Invitation code is invalid": "Einladungscode ist ungültig",
|
||||
"Invitation code suspended": "Einladungscode ausgesetzt",
|
||||
"LDAP user name or password incorrect": "Ldap Benutzername oder Passwort falsch",
|
||||
"LastName cannot be blank": "Nachname darf nicht leer sein",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Mehrere Konten mit derselben uid, bitte überprüfen Sie Ihren LDAP-Server",
|
||||
"Organization does not exist": "Organisation existiert nicht",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Gruppen importieren fehlgeschlagen",
|
||||
"Failed to import users": "Fehler beim Importieren von Benutzern",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Fehlender Parameter",
|
||||
"Only admin user can specify user": "Nur Administrator kann Benutzer angeben",
|
||||
"Please login first": "Bitte zuerst einloggen",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "Die Organisation: %s sollte mindestens eine Anwendung haben",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "Der Benutzer %s existiert nicht",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "Falsche Benutzer-ID",
|
||||
"don't support captchaProvider: ": "Unterstütze captchaProvider nicht:",
|
||||
"this operation is not allowed in demo mode": "Dieser Vorgang ist im Demo-Modus nicht erlaubt",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "Die Berechtigung: \"%s\" existiert nicht"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Ungültige Anwendungs-ID",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "Der Anbieter %s existiert nicht"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Ungültige E-Mail-Empfänger: %s",
|
||||
"Invalid phone receivers: %s": "Ungültige Telefonempfänger: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "Der Objektschlüssel %s ist nicht erlaubt",
|
||||
"The provider type: %s is not supported": "Der Anbieter-Typ %s wird nicht unterstützt"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Fehler"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s wird von dieser Anwendung nicht unterstützt",
|
||||
"Invalid application or wrong clientSecret": "Ungültige Anwendung oder falsches clientSecret",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Anzeigename darf nicht leer sein",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA-E-Mail ist aktiviert, aber E-Mail ist leer",
|
||||
"MFA phone is enabled but phone number is empty": "MFA-Telefon ist aktiviert, aber Telefonnummer ist leer",
|
||||
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "Das neue Passwort muss sich von Ihrem aktuellen Passwort unterscheiden",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "Eigentümer und Name des Benutzers dürfen nicht leer sein"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Ungültiger Captcha-Anbieter.",
|
||||
"Phone number is invalid in your region %s": "Die Telefonnummer ist in Ihrer Region %s ungültig",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "Der Verifizierungscode wurde bereits verwendet!",
|
||||
"The verification code has not been sent yet!": "Der Verifizierungscode wurde noch nicht gesendet!",
|
||||
"Turing test failed.": "Turing-Test fehlgeschlagen.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Failed to add user",
|
||||
"Get init score failed, error: %w": "Get init score failed, error: %w",
|
||||
"Please sign out first": "Please sign out first",
|
||||
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application",
|
||||
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application",
|
||||
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "The organization: %s does not exist",
|
||||
"The organization: %s has disabled users to signin": "The organization: %s has disabled users to signin",
|
||||
"The plan: %s does not exist": "The plan: %s does not exist",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Face data mismatch",
|
||||
"Failed to parse client IP: %s": "Failed to parse client IP: %s",
|
||||
"FirstName cannot be blank": "FirstName cannot be blank",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "Invitation code cannot be blank",
|
||||
"Invitation code exhausted": "Invitation code exhausted",
|
||||
"Invitation code is invalid": "Invitation code is invalid",
|
||||
"Invitation code suspended": "Invitation code suspended",
|
||||
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
|
||||
"LastName cannot be blank": "LastName cannot be blank",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
|
||||
"Organization does not exist": "Organization does not exist",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Failed to import groups",
|
||||
"Failed to import users": "Failed to import users",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Missing parameter",
|
||||
"Only admin user can specify user": "Only admin user can specify user",
|
||||
"Please login first": "Please login first",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "The organization: %s should have one application at least",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "The user: %s doesn't exist",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "Wrong userId",
|
||||
"don't support captchaProvider: ": "don't support captchaProvider: ",
|
||||
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "The permission: \"%s\" doesn't exist"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Invalid application id",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "the provider: %s does not exist"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
|
||||
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
|
||||
"The provider type: %s is not supported": "The provider type: %s is not supported"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Error"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
|
||||
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Display name cannot be empty",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA email is enabled but email is empty",
|
||||
"MFA phone is enabled but phone number is empty": "MFA phone is enabled but phone number is empty",
|
||||
"New password cannot contain blank space.": "New password cannot contain blank space.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "The new password must be different from your current password",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Invalid captcha provider.",
|
||||
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "The verification code has already been used!",
|
||||
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
|
||||
"Turing test failed.": "Turing test failed.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "No se pudo agregar el usuario",
|
||||
"Get init score failed, error: %w": "Error al obtener el puntaje de inicio, error: %w",
|
||||
"Please sign out first": "Por favor, cierra sesión primero",
|
||||
"The application does not allow to sign up new account": "La aplicación no permite registrarse con una cuenta nueva"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "El método de inicio de sesión: inicio de sesión con correo electrónico no está habilitado para la aplicación",
|
||||
"The login method: login with face is not enabled for the application": "El método de inicio de sesión: inicio de sesión con reconocimiento facial no está habilitado para la aplicación",
|
||||
"The login method: login with password is not enabled for the application": "El método de inicio de sesión: inicio de sesión con contraseña no está habilitado para la aplicación",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "La organización: %s no existe",
|
||||
"The organization: %s has disabled users to signin": "La organización: %s ha desactivado el inicio de sesión de usuarios",
|
||||
"The plan: %s does not exist": "El plan: %s no existe",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Los datos faciales no coinciden",
|
||||
"Failed to parse client IP: %s": "Error al analizar la IP del cliente: %s",
|
||||
"FirstName cannot be blank": "El nombre no puede estar en blanco",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "El código de invitación no puede estar vacío",
|
||||
"Invitation code exhausted": "Código de invitación agotado",
|
||||
"Invitation code is invalid": "Código de invitación inválido",
|
||||
"Invitation code suspended": "Código de invitación suspendido",
|
||||
"LDAP user name or password incorrect": "Nombre de usuario o contraseña de Ldap incorrectos",
|
||||
"LastName cannot be blank": "El apellido no puede estar en blanco",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Cuentas múltiples con el mismo uid, por favor revise su servidor ldap",
|
||||
"Organization does not exist": "La organización no existe",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Error al importar grupos",
|
||||
"Failed to import users": "Error al importar usuarios",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Parámetro faltante",
|
||||
"Only admin user can specify user": "Solo el usuario administrador puede especificar usuario",
|
||||
"Please login first": "Por favor, inicia sesión primero",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "La organización: %s debe tener al menos una aplicación",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "El usuario: %s no existe",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "ID de usuario incorrecto",
|
||||
"don't support captchaProvider: ": "No apoyo a captchaProvider",
|
||||
"this operation is not allowed in demo mode": "esta operación no está permitida en modo de demostración",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "El permiso: \"%s\" no existe"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Identificación de aplicación no válida",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "El proveedor: %s no existe"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Receptores de correo electrónico no válidos: %s",
|
||||
"Invalid phone receivers: %s": "Receptores de teléfono no válidos: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "El objectKey: %s no está permitido",
|
||||
"The provider type: %s is not supported": "El tipo de proveedor: %s no es compatible"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Error"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "El tipo de subvención: %s no es compatible con esta aplicación",
|
||||
"Invalid application or wrong clientSecret": "Solicitud inválida o clientSecret incorrecto",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "El nombre de pantalla no puede estar vacío",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "El correo electrónico MFA está habilitado pero el correo está vacío",
|
||||
"MFA phone is enabled but phone number is empty": "El teléfono MFA está habilitado pero el número de teléfono está vacío",
|
||||
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "La nueva contraseña debe ser diferente de su contraseña actual",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "el propietario y el nombre del usuario no deben estar vacíos"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"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",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "¡El código de verificación ya ha sido utilizado!",
|
||||
"The verification code has not been sent yet!": "¡El código de verificación aún no ha sido enviado!",
|
||||
"Turing test failed.": "El test de Turing falló.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Échec d'ajout d'utilisateur",
|
||||
"Get init score failed, error: %w": "Obtention du score initiale échouée, erreur : %w",
|
||||
"Please sign out first": "Veuillez vous déconnecter en premier",
|
||||
"The application does not allow to sign up new account": "L'application ne permet pas de créer un nouveau compte"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "La méthode de connexion : connexion par e-mail n'est pas activée pour l'application",
|
||||
"The login method: login with face is not enabled for the application": "La méthode de connexion : connexion par visage n'est pas activée pour l'application",
|
||||
"The login method: login with password is not enabled for the application": "La méthode de connexion : connexion avec mot de passe n'est pas activée pour l'application",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "L'organisation : %s n'existe pas",
|
||||
"The organization: %s has disabled users to signin": "L'organisation: %s a désactivé la connexion des utilisateurs",
|
||||
"The plan: %s does not exist": "Le plan : %s n'existe pas",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Données faciales incorrectes",
|
||||
"Failed to parse client IP: %s": "Échec de l'analyse de l'IP client : %s",
|
||||
"FirstName cannot be blank": "Le prénom ne peut pas être laissé vide",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "Le code d'invitation ne peut pas être vide",
|
||||
"Invitation code exhausted": "Code d'invitation épuisé",
|
||||
"Invitation code is invalid": "Code d'invitation invalide",
|
||||
"Invitation code suspended": "Code d'invitation suspendu",
|
||||
"LDAP user name or password incorrect": "Nom d'utilisateur ou mot de passe LDAP incorrect",
|
||||
"LastName cannot be blank": "Le nom de famille ne peut pas être vide",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Plusieurs comptes avec le même identifiant d'utilisateur, veuillez vérifier votre serveur LDAP",
|
||||
"Organization does not exist": "L'organisation n'existe pas",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Échec de l'importation des groupes",
|
||||
"Failed to import users": "Échec de l'importation des utilisateurs",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Paramètre manquant",
|
||||
"Only admin user can specify user": "Seul un administrateur peut désigner un utilisateur",
|
||||
"Please login first": "Veuillez d'abord vous connecter",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "L'organisation : %s doit avoir au moins une application",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "L'utilisateur : %s n'existe pas",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "ID utilisateur incorrect",
|
||||
"don't support captchaProvider: ": "ne prend pas en charge captchaProvider: ",
|
||||
"this operation is not allowed in demo mode": "cette opération n'est pas autorisée en mode démo",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "La permission : \"%s\" n'existe pas"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Identifiant d'application invalide",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "Le fournisseur : %s n'existe pas"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Destinataires d'e-mail invalides : %s",
|
||||
"Invalid phone receivers: %s": "Destinataires de téléphone invalide : %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "La clé d'objet : %s n'est pas autorisée",
|
||||
"The provider type: %s is not supported": "Le type de fournisseur : %s n'est pas pris en charge"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Erreur"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Type_de_subvention : %s n'est pas pris en charge dans cette application",
|
||||
"Invalid application or wrong clientSecret": "Application invalide ou clientSecret incorrect",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Le nom d'affichage ne peut pas être vide",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "L'authentification MFA par e-mail est activée mais l'e-mail est vide",
|
||||
"MFA phone is enabled but phone number is empty": "L'authentification MFA par téléphone est activée mais le numéro de téléphone est vide",
|
||||
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "Le nouveau mot de passe doit être différent de votre mot de passe actuel",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "le propriétaire et le nom de l'utilisateur ne doivent pas être vides"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"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",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "Le code de vérification a déjà été utilisé !",
|
||||
"The verification code has not been sent yet!": "Le code de vérification n'a pas encore été envoyé !",
|
||||
"Turing test failed.": "Le test de Turing a échoué.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "ユーザーの追加に失敗しました",
|
||||
"Get init score failed, error: %w": "イニットスコアの取得に失敗しました。エラー:%w",
|
||||
"Please sign out first": "最初にサインアウトしてください",
|
||||
"The application does not allow to sign up new account": "アプリケーションは新しいアカウントの登録を許可しません"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "このアプリケーションではメールログインは有効になっていません",
|
||||
"The login method: login with face is not enabled for the application": "このアプリケーションでは顔認証ログインは有効になっていません",
|
||||
"The login method: login with password is not enabled for the application": "ログイン方法:パスワードでのログインはアプリケーションで有効になっていません",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "組織「%s」は存在しません",
|
||||
"The organization: %s has disabled users to signin": "組織: %s はユーザーのサインインを無効にしました",
|
||||
"The plan: %s does not exist": "プラン: %sは存在しません",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "顔認証データが一致しません",
|
||||
"Failed to parse client IP: %s": "クライアント IP「%s」の解析に失敗しました",
|
||||
"FirstName cannot be blank": "ファーストネームは空白にできません",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "招待コードは空にできません",
|
||||
"Invitation code exhausted": "招待コードの使用回数が上限に達しました",
|
||||
"Invitation code is invalid": "招待コードが無効です",
|
||||
"Invitation code suspended": "招待コードは一時的に無効化されています",
|
||||
"LDAP user name or password incorrect": "Ldapのユーザー名またはパスワードが間違っています",
|
||||
"LastName cannot be blank": "姓は空白にできません",
|
||||
"Multiple accounts with same uid, please check your ldap server": "同じuidを持つ複数のアカウントがあります。あなたのLDAPサーバーを確認してください",
|
||||
"Organization does not exist": "組織は存在しません",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "グループのインポートに失敗しました",
|
||||
"Failed to import users": "ユーザーのインポートに失敗しました",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "不足しているパラメーター",
|
||||
"Only admin user can specify user": "管理者ユーザーのみがユーザーを指定できます",
|
||||
"Please login first": "最初にログインしてください",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "組織「%s」は少なくとも1つのアプリケーションを持っている必要があります",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "そのユーザー:%sは存在しません",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "無効なユーザーIDです",
|
||||
"don't support captchaProvider: ": "captchaProviderをサポートしないでください",
|
||||
"this operation is not allowed in demo mode": "この操作はデモモードでは許可されていません",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "権限「%s」は存在しません"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "アプリケーションIDが無効です",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "プロバイダー%sは存在しません"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "無効な電子メール受信者:%s",
|
||||
"Invalid phone receivers: %s": "電話受信者が無効です:%s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "オブジェクトキー %s は許可されていません",
|
||||
"The provider type: %s is not supported": "プロバイダータイプ:%sはサポートされていません"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "エラー"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "grant_type:%sはこのアプリケーションでサポートされていません",
|
||||
"Invalid application or wrong clientSecret": "無効なアプリケーションまたは誤ったクライアントシークレットです",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "表示名は空にできません",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA メールが有効になっていますが、メールアドレスが空です",
|
||||
"MFA phone is enabled but phone number is empty": "MFA 電話番号が有効になっていますが、電話番号が空です",
|
||||
"New password cannot contain blank space.": "新しいパスワードにはスペースを含めることはできません。",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "新しいパスワードは現在のパスワードと異なる必要があります",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "ユーザーのオーナーと名前は空にできません"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "無効なCAPTCHAプロバイダー。",
|
||||
"Phone number is invalid in your region %s": "電話番号はあなたの地域で無効です %s",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "この検証コードは既に使用されています!",
|
||||
"The verification code has not been sent yet!": "検証コードはまだ送信されていません!",
|
||||
"Turing test failed.": "チューリングテストは失敗しました。",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Nie udało się dodać użytkownika",
|
||||
"Get init score failed, error: %w": "Pobranie początkowego wyniku nie powiodło się, błąd: %w",
|
||||
"Please sign out first": "Najpierw się wyloguj",
|
||||
"The application does not allow to sign up new account": "Aplikacja nie pozwala na rejestrację nowego konta"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "Metoda logowania: logowanie przez email nie jest włączona dla aplikacji",
|
||||
"The login method: login with face is not enabled for the application": "Metoda logowania: logowanie przez twarz nie jest włączona dla aplikacji",
|
||||
"The login method: login with password is not enabled for the application": "Metoda logowania: logowanie przez hasło nie jest włączone dla aplikacji",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "Organizacja: %s nie istnieje",
|
||||
"The organization: %s has disabled users to signin": "Organizacja: %s wyłączyła logowanie użytkowników",
|
||||
"The plan: %s does not exist": "Plan: %s nie istnieje",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Niezgodność danych twarzy",
|
||||
"Failed to parse client IP: %s": "Nie udało się przeanalizować IP klienta: %s",
|
||||
"FirstName cannot be blank": "Imię nie może być puste",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "Kod zaproszenia nie może być pusty",
|
||||
"Invitation code exhausted": "Kod zaproszenia został wykorzystany",
|
||||
"Invitation code is invalid": "Kod zaproszenia jest nieprawidłowy",
|
||||
"Invitation code suspended": "Kod zaproszenia został zawieszony",
|
||||
"LDAP user name or password incorrect": "Nazwa użytkownika LDAP lub hasło jest nieprawidłowe",
|
||||
"LastName cannot be blank": "Nazwisko nie może być puste",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Wiele kont z tym samym uid, sprawdź swój serwer ldap",
|
||||
"Organization does not exist": "Organizacja nie istnieje",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Nie udało się zaimportować grup",
|
||||
"Failed to import users": "Nie udało się zaimportować użytkowników",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Brakujący parametr",
|
||||
"Only admin user can specify user": "Tylko administrator może wskazać użytkownika",
|
||||
"Please login first": "Najpierw się zaloguj",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "Organizacja: %s powinna mieć co najmniej jedną aplikację",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "Użytkownik: %s nie istnieje",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "Nieprawidłowy userId",
|
||||
"don't support captchaProvider: ": "nie obsługuje captchaProvider: ",
|
||||
"this operation is not allowed in demo mode": "ta operacja nie jest dozwolona w trybie demo",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "Uprawnienie: \"%s\" nie istnieje"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Nieprawidłowe id aplikacji",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "dostawca: %s nie istnieje"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Nieprawidłowi odbiorcy email: %s",
|
||||
"Invalid phone receivers: %s": "Nieprawidłowi odbiorcy telefonu: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "Klucz obiektu: %s jest niedozwolony",
|
||||
"The provider type: %s is not supported": "Typ dostawcy: %s nie jest obsługiwany"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Błąd"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s nie jest obsługiwany w tej aplikacji",
|
||||
"Invalid application or wrong clientSecret": "Nieprawidłowa aplikacja lub błędny clientSecret",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Nazwa wyświetlana nie może być pusta",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA email jest włączone, ale email jest pusty",
|
||||
"MFA phone is enabled but phone number is empty": "MFA telefon jest włączony, ale numer telefonu jest pusty",
|
||||
"New password cannot contain blank space.": "Nowe hasło nie może zawierać spacji.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "Nowe hasło musi różnić się od obecnego hasła",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "właściciel i nazwa użytkownika nie powinny być puste"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Nieprawidłowy dostawca captcha.",
|
||||
"Phone number is invalid in your region %s": "Numer telefonu jest nieprawidłowy w twoim regionie %s",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "Kod weryfikacyjny został już wykorzystany!",
|
||||
"The verification code has not been sent yet!": "Kod weryfikacyjny nie został jeszcze wysłany!",
|
||||
"Turing test failed.": "Test Turinga nie powiódł się.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Falha ao adicionar usuário",
|
||||
"Get init score failed, error: %w": "Falha ao obter pontuação inicial, erro: %w",
|
||||
"Please sign out first": "Por favor, saia primeiro",
|
||||
"The application does not allow to sign up new account": "O aplicativo não permite a criação de novas contas"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "O método de login com e-mail não está habilitado para o aplicativo",
|
||||
"The login method: login with face is not enabled for the application": "O método de login com reconhecimento facial não está habilitado para o aplicativo",
|
||||
"The login method: login with password is not enabled for the application": "O método de login com senha não está habilitado para o aplicativo",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "A organização: %s não existe",
|
||||
"The organization: %s has disabled users to signin": "A organização: %s desativou o login de usuários",
|
||||
"The plan: %s does not exist": "O plano: %s não existe",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Dados faciais não correspondem",
|
||||
"Failed to parse client IP: %s": "Falha ao analisar o IP do cliente: %s",
|
||||
"FirstName cannot be blank": "O primeiro nome não pode estar em branco",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "O código de convite não pode estar em branco",
|
||||
"Invitation code exhausted": "O código de convite foi esgotado",
|
||||
"Invitation code is invalid": "Código de convite inválido",
|
||||
"Invitation code suspended": "Código de convite suspenso",
|
||||
"LDAP user name or password incorrect": "Nome de usuário ou senha LDAP incorretos",
|
||||
"LastName cannot be blank": "O sobrenome não pode estar em branco",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Múltiplas contas com o mesmo uid, verifique seu servidor LDAP",
|
||||
"Organization does not exist": "A organização não existe",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Falha ao importar grupos",
|
||||
"Failed to import users": "Falha ao importar usuários",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Parâmetro ausente",
|
||||
"Only admin user can specify user": "Apenas um administrador pode especificar um usuário",
|
||||
"Please login first": "Por favor, faça login primeiro",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "A organização: %s deve ter pelo menos um aplicativo",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "O usuário: %s não existe",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "ID de usuário incorreto",
|
||||
"don't support captchaProvider: ": "captchaProvider não suportado: ",
|
||||
"this operation is not allowed in demo mode": "esta operação não é permitida no modo de demonstração",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "A permissão: \"%s\" não existe"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "ID de aplicativo inválido",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "O provedor: %s não existe"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Destinatários de e-mail inválidos: %s",
|
||||
"Invalid phone receivers: %s": "Destinatários de telefone inválidos: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "A chave de objeto: %s não é permitida",
|
||||
"The provider type: %s is not supported": "O tipo de provedor: %s não é suportado"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Erro"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s não é suportado neste aplicativo",
|
||||
"Invalid application or wrong clientSecret": "Aplicativo inválido ou clientSecret incorreto",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "O nome de exibição não pode estar vazio",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA por e-mail está habilitado, mas o e-mail está vazio",
|
||||
"MFA phone is enabled but phone number is empty": "MFA por telefone está habilitado, mas o número de telefone está vazio",
|
||||
"New password cannot contain blank space.": "A nova senha não pode conter espaços em branco.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "A nova senha deve ser diferente da senha atual",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "O proprietário e o nome do usuário não devem estar vazios"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Provedor de captcha inválido.",
|
||||
"Phone number is invalid in your region %s": "Número de telefone inválido na sua região %s",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "O código de verificação já foi utilizado!",
|
||||
"The verification code has not been sent yet!": "O código de verificação ainda não foi enviado!",
|
||||
"Turing test failed.": "O teste de Turing falhou.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Kullanıcı eklenemedi",
|
||||
"Get init score failed, error: %w": "Başlangıç puanı alınamadı, hata: %w",
|
||||
"Please sign out first": "Lütfen önce çıkış yapın",
|
||||
"The application does not allow to sign up new account": "Uygulama yeni hesap kaydına izin vermiyor"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "Uygulama için e-posta ile giriş yöntemi etkin değil",
|
||||
"The login method: login with face is not enabled for the application": "Uygulama için yüz ile giriş yöntemi etkin değil",
|
||||
"The login method: login with password is not enabled for the application": "Şifre ile giriş yöntemi bu uygulama için etkin değil",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "Organizasyon: %s mevcut değil",
|
||||
"The organization: %s has disabled users to signin": "Organizasyon: %s kullanıcıların oturum açmasını devre dışı bıraktı",
|
||||
"The plan: %s does not exist": "Plan: %s mevcut değil",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Yüz verisi uyuşmazlığı",
|
||||
"Failed to parse client IP: %s": "İstemci IP'si ayrıştırılamadı: %s",
|
||||
"FirstName cannot be blank": "Ad boş olamaz",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "Davet kodu boş olamaz",
|
||||
"Invitation code exhausted": "Davet kodu kullanım dışı",
|
||||
"Invitation code is invalid": "Davet kodu geçersiz",
|
||||
"Invitation code suspended": "Davet kodu askıya alındı",
|
||||
"LDAP user name or password incorrect": "LDAP kullanıcı adı veya şifre yanlış",
|
||||
"LastName cannot be blank": "Soyad boş olamaz",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Aynı uid'ye sahip birden fazla hesap, lütfen ldap sunucunuzu kontrol edin",
|
||||
"Organization does not exist": "Organizasyon bulunamadı",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Gruplar içe aktarılamadı",
|
||||
"Failed to import users": "Kullanıcılar içe aktarılamadı",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Eksik parametre",
|
||||
"Only admin user can specify user": "Yalnızca yönetici kullanıcı kullanıcı belirleyebilir",
|
||||
"Please login first": "Lütfen önce giriş yapın",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "Organizasyon: %s en az bir uygulamaya sahip olmalı",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "Kullanıcı: %s bulunamadı",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "Yanlış kullanıcı kimliği",
|
||||
"don't support captchaProvider: ": "captchaProvider desteklenmiyor: ",
|
||||
"this operation is not allowed in demo mode": "bu işlem demo modunda izin verilmiyor",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "İzin: \"%s\" mevcut değil"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Geçersiz uygulama id",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "provider: %s bulunamadı"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Geçersiz e-posta alıcıları: %s",
|
||||
"Invalid phone receivers: %s": "Geçersiz telefon alıcıları: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "objectKey: %s izin verilmiyor",
|
||||
"The provider type: %s is not supported": "provider türü: %s desteklenmiyor"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Hata"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s bu uygulamada desteklenmiyor",
|
||||
"Invalid application or wrong clientSecret": "Geçersiz uygulama veya yanlış clientSecret",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Görünen ad boş olamaz",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA e-postası etkin ancak e-posta boş",
|
||||
"MFA phone is enabled but phone number is empty": "MFA telefonu etkin ancak telefon numarası boş",
|
||||
"New password cannot contain blank space.": "Yeni şifre boşluk içeremez.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "Yeni şifre mevcut şifrenizden farklı olmalıdır",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "kullanıcının sahibi ve adı boş olmamalıdır"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Geçersiz captcha sağlayıcı.",
|
||||
"Phone number is invalid in your region %s": "Telefon numaranız bölgenizde geçersiz %s",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "Doğrulama kodu zaten kullanılmış!",
|
||||
"The verification code has not been sent yet!": "Doğrulama kodu henüz gönderilmedi!",
|
||||
"Turing test failed.": "Turing testi başarısız.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Не вдалося додати користувача",
|
||||
"Get init score failed, error: %w": "Не вдалося отримати початковий бал, помилка: %w",
|
||||
"Please sign out first": "Спочатку вийдіть із системи",
|
||||
"The application does not allow to sign up new account": "Додаток не дозволяє реєструвати нові облікові записи"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "Метод входу через email не увімкнено для цього додатка",
|
||||
"The login method: login with face is not enabled for the application": "Метод входу через обличчя не увімкнено для цього додатка",
|
||||
"The login method: login with password is not enabled for the application": "Метод входу через пароль не увімкнено для цього додатка",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "Організація: %s не існує",
|
||||
"The organization: %s has disabled users to signin": "Організація: %s вимкнула вхід користувачів",
|
||||
"The plan: %s does not exist": "План: %s не існує",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Невідповідність даних обличчя",
|
||||
"Failed to parse client IP: %s": "Не вдалося розібрати IP клієнта: %s",
|
||||
"FirstName cannot be blank": "Ім’я не може бути порожнім",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "Код запрошення не може бути порожнім",
|
||||
"Invitation code exhausted": "Код запрошення вичерпано",
|
||||
"Invitation code is invalid": "Код запрошення недійсний",
|
||||
"Invitation code suspended": "Код запрошення призупинено",
|
||||
"LDAP user name or password incorrect": "Ім’я користувача або пароль LDAP неправильні",
|
||||
"LastName cannot be blank": "Прізвище не може бути порожнім",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Кілька облікових записів з однаковим uid, перевірте ваш ldap-сервер",
|
||||
"Organization does not exist": "Організація не існує",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Не вдалося імпортувати групи",
|
||||
"Failed to import users": "Не вдалося імпортувати користувачів",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Відсутній параметр",
|
||||
"Only admin user can specify user": "Лише адміністратор може вказати користувача",
|
||||
"Please login first": "Спочатку увійдіть",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "Організація: %s має мати щонайменше один додаток",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "Користувач: %s не існує",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "Неправильний userId",
|
||||
"don't support captchaProvider: ": "не підтримується captchaProvider: ",
|
||||
"this operation is not allowed in demo mode": "ця операція недоступна в демо-режимі",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "Behörigheten: \"%s\" finns inte"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Недійсний id додатка",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "провайдер: %s не існує"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Недійсні отримувачі Email: %s",
|
||||
"Invalid phone receivers: %s": "Недійсні отримувачі телефону: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "objectKey: %s не дозволено",
|
||||
"The provider type: %s is not supported": "Тип провайдера: %s не підтримується"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Помилка"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s не підтримується в цьому додатку",
|
||||
"Invalid application or wrong clientSecret": "Недійсний додаток або неправильний clientSecret",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Відображуване ім’я не може бути порожнім",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA email увімкнено, але email порожній",
|
||||
"MFA phone is enabled but phone number is empty": "MFA телефон увімкнено, але номер телефону порожній",
|
||||
"New password cannot contain blank space.": "Новий пароль не може містити пробіли.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "Новий пароль повинен відрізнятися від поточного пароля",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "власник ім’я користувача не повинні бути порожніми"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Недійсний провайдер captcha.",
|
||||
"Phone number is invalid in your region %s": "Номер телефону недійсний у вашому регіоні %s",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "Код підтвердження вже використано!",
|
||||
"The verification code has not been sent yet!": "Код підтвердження ще не надіслано!",
|
||||
"Turing test failed.": "Тест Тюрінга не пройдено.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "Không thể thêm người dùng",
|
||||
"Get init score failed, error: %w": "Lấy điểm khởi đầu thất bại, lỗi: %w",
|
||||
"Please sign out first": "Vui lòng đăng xuất trước",
|
||||
"The application does not allow to sign up new account": "Ứng dụng không cho phép đăng ký tài khoản mới"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "Phương thức đăng nhập bằng email chưa được bật cho ứng dụng",
|
||||
"The login method: login with face is not enabled for the application": "Phương thức đăng nhập bằng khuôn mặt chưa được bật cho ứng dụng",
|
||||
"The login method: login with password is not enabled for the application": "Phương thức đăng nhập: đăng nhập bằng mật khẩu không được kích hoạt cho ứng dụng",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "Tổ chức: %s không tồn tại",
|
||||
"The organization: %s has disabled users to signin": "Tổ chức: %s đã vô hiệu hóa đăng nhập của người dùng",
|
||||
"The plan: %s does not exist": "Kế hoạch: %s không tồn tại",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "Dữ liệu khuôn mặt không khớp",
|
||||
"Failed to parse client IP: %s": "Không thể phân tích IP khách: %s",
|
||||
"FirstName cannot be blank": "Tên không được để trống",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "Mã mời không được để trống",
|
||||
"Invitation code exhausted": "Mã mời đã hết",
|
||||
"Invitation code is invalid": "Mã mời không hợp lệ",
|
||||
"Invitation code suspended": "Mã mời đã bị tạm ngưng",
|
||||
"LDAP user name or password incorrect": "Tên người dùng hoặc mật khẩu Ldap không chính xác",
|
||||
"LastName cannot be blank": "Họ không thể để trống",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Nhiều tài khoản với cùng một uid, vui lòng kiểm tra máy chủ ldap của bạn",
|
||||
"Organization does not exist": "Tổ chức không tồn tại",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Không thể nhập nhóm",
|
||||
"Failed to import users": "Không thể nhập người dùng",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "Thiếu tham số",
|
||||
"Only admin user can specify user": "Chỉ người dùng quản trị mới có thể chỉ định người dùng",
|
||||
"Please login first": "Vui lòng đăng nhập trước",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "Tổ chức: %s cần có ít nhất một ứng dụng",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "Người dùng: %s không tồn tại",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "ID người dùng sai",
|
||||
"don't support captchaProvider: ": "không hỗ trợ captchaProvider: ",
|
||||
"this operation is not allowed in demo mode": "thao tác này không được phép trong chế độ demo",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "Quyền: \"%s\" không tồn tại"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "Sai ID ứng dụng",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "Nhà cung cấp: %s không tồn tại"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "Người nhận Email không hợp lệ: %s",
|
||||
"Invalid phone receivers: %s": "Người nhận điện thoại không hợp lệ: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "Khóa đối tượng: %s không được phép",
|
||||
"The provider type: %s is not supported": "Loại nhà cung cấp: %s không được hỗ trợ"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "Lỗi"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Loại cấp phép: %s không được hỗ trợ trong ứng dụng này",
|
||||
"Invalid application or wrong clientSecret": "Đơn đăng ký không hợp lệ hoặc sai clientSecret",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Tên hiển thị không thể trống",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA email đã bật nhưng email trống",
|
||||
"MFA phone is enabled but phone number is empty": "MFA điện thoại đã bật nhưng số điện thoại trống",
|
||||
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng.",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "Mật khẩu mới phải khác với mật khẩu hiện tại của bạn",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "chủ sở hữu và tên người dùng không được để trống"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"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",
|
||||
"The forgot password feature is disabled": "The forgot password feature is disabled",
|
||||
"The verification code has already been used!": "Mã xác thực đã được sử dụng!",
|
||||
"The verification code has not been sent yet!": "Mã xác thực chưa được gửi!",
|
||||
"Turing test failed.": "Kiểm định Turing thất bại.",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"account": {
|
||||
"Failed to add user": "添加用户失败",
|
||||
"Get init score failed, error: %w": "初始化分数失败: %w",
|
||||
"Please sign out first": "请先退出登录",
|
||||
"The application does not allow to sign up new account": "该应用不允许注册新用户"
|
||||
},
|
||||
"auth": {
|
||||
@@ -23,6 +22,7 @@
|
||||
"The login method: login with email is not enabled for the application": "该应用禁止采用邮箱登录方式",
|
||||
"The login method: login with face is not enabled for the application": "该应用禁止采用人脸登录",
|
||||
"The login method: login with password is not enabled for the application": "该应用禁止采用密码登录方式",
|
||||
"The order: %s does not exist": "The order: %s does not exist",
|
||||
"The organization: %s does not exist": "组织: %s 不存在",
|
||||
"The organization: %s has disabled users to signin": "组织: %s 禁止用户登录",
|
||||
"The plan: %s does not exist": "计划: %s不存在",
|
||||
@@ -57,11 +57,11 @@
|
||||
"Face data mismatch": "人脸不匹配",
|
||||
"Failed to parse client IP: %s": "无法解析客户端 IP 地址: %s",
|
||||
"FirstName cannot be blank": "名不可以为空",
|
||||
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
|
||||
"Invitation code cannot be blank": "邀请码不能为空",
|
||||
"Invitation code exhausted": "邀请码使用次数已耗尽",
|
||||
"Invitation code is invalid": "邀请码无效",
|
||||
"Invitation code suspended": "邀请码已被禁止使用",
|
||||
"LDAP user name or password incorrect": "LDAP密码错误",
|
||||
"LastName cannot be blank": "姓不可以为空",
|
||||
"Multiple accounts with same uid, please check your ldap server": "多个帐户具有相同的uid,请检查您的 LDAP 服务器",
|
||||
"Organization does not exist": "组织不存在",
|
||||
@@ -106,11 +106,17 @@
|
||||
"general": {
|
||||
"Failed to import groups": "导入群组失败",
|
||||
"Failed to import users": "导入用户失败",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Missing parameter": "缺少参数",
|
||||
"Only admin user can specify user": "仅管理员用户可以指定用户",
|
||||
"Please login first": "请先登录",
|
||||
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
|
||||
"The organization: %s should have one application at least": "组织: %s 应该拥有至少一个应用",
|
||||
"The syncer: %s does not exist": "The syncer: %s does not exist",
|
||||
"The user: %s doesn't exist": "用户: %s不存在",
|
||||
"The user: %s is not found": "The user: %s is not found",
|
||||
"User is required for User category transaction": "User is required for User category transaction",
|
||||
"Wrong userId": "错误的 userId",
|
||||
"don't support captchaProvider: ": "不支持验证码提供商: ",
|
||||
"this operation is not allowed in demo mode": "demo模式下不允许该操作",
|
||||
@@ -139,8 +145,14 @@
|
||||
"permission": {
|
||||
"The permission: \"%s\" doesn't exist": "权限: \"%s\" 不存在"
|
||||
},
|
||||
"product": {
|
||||
"Product list cannot be empty": "Product list cannot be empty"
|
||||
},
|
||||
"provider": {
|
||||
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
|
||||
"Invalid application id": "无效的应用ID",
|
||||
"No ID Verification provider configured": "No ID Verification provider configured",
|
||||
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
|
||||
"the provider: %s does not exist": "提供商: %s不存在"
|
||||
},
|
||||
"resource": {
|
||||
@@ -158,6 +170,9 @@
|
||||
"Invalid Email receivers: %s": "无效的邮箱收件人: %s",
|
||||
"Invalid phone receivers: %s": "无效的手机短信收信人: %s"
|
||||
},
|
||||
"session": {
|
||||
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "objectKey: %s被禁止",
|
||||
"The provider type: %s is not supported": "不支持的提供商类型: %s"
|
||||
@@ -165,6 +180,9 @@
|
||||
"subscription": {
|
||||
"Error": "错误"
|
||||
},
|
||||
"ticket": {
|
||||
"Ticket not found": "Ticket not found"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "该应用不支持Grant_type: %s",
|
||||
"Invalid application or wrong clientSecret": "无效应用或错误的clientSecret",
|
||||
@@ -174,10 +192,14 @@
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "显示名称不可为空",
|
||||
"ID card information and real name are required": "ID card information and real name are required",
|
||||
"Identity verification failed": "Identity verification failed",
|
||||
"MFA email is enabled but email is empty": "MFA 电子邮件已启用,但电子邮件为空",
|
||||
"MFA phone is enabled but phone number is empty": "MFA 电话已启用,但电话号码为空",
|
||||
"New password cannot contain blank space.": "新密码不可以包含空格",
|
||||
"No application found for user": "No application found for user",
|
||||
"The new password must be different from your current password": "新密码必须与您当前的密码不同",
|
||||
"User is already verified": "User is already verified",
|
||||
"the user's owner and name should not be empty": "用户的组织和名称不能为空"
|
||||
},
|
||||
"util": {
|
||||
@@ -188,6 +210,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "非法的验证码提供商",
|
||||
"Phone number is invalid in your region %s": "您所在地区的电话号码无效 %s",
|
||||
"The forgot password feature is disabled": "忘记密码功能已被禁用",
|
||||
"The verification code has already been used!": "验证码已使用过!",
|
||||
"The verification code has not been sent yet!": "验证码未发送!",
|
||||
"Turing test failed.": "验证码还未发送",
|
||||
|
||||
@@ -216,6 +216,16 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
|
||||
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
|
||||
e.AddAttribute("sn", message.AttributeValue(user.LastName))
|
||||
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
|
||||
// Add POSIX attributes for Linux machine login support
|
||||
e.AddAttribute("loginShell", getAttribute("loginShell", user))
|
||||
e.AddAttribute("gecos", getAttribute("gecos", user))
|
||||
// Add SSH public key if available
|
||||
sshKey := getAttribute("sshPublicKey", user)
|
||||
if sshKey != "" {
|
||||
e.AddAttribute("sshPublicKey", sshKey)
|
||||
}
|
||||
// Add objectClass for posixAccount
|
||||
e.AddAttribute("objectClass", "posixAccount")
|
||||
for _, group := range user.Groups {
|
||||
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
|
||||
}
|
||||
|
||||
39
ldap/util.go
39
ldap/util.go
@@ -83,6 +83,45 @@ var ldapAttributesMapping = map[string]FieldRelation{
|
||||
return message.AttributeValue(getUserPasswordWithType(user))
|
||||
},
|
||||
},
|
||||
"loginShell": {
|
||||
userField: "loginShell",
|
||||
notSearchable: true,
|
||||
fieldMapper: func(user *object.User) message.AttributeValue {
|
||||
// Check user properties first, otherwise return default shell
|
||||
if user.Properties != nil {
|
||||
if shell, ok := user.Properties["loginShell"]; ok && shell != "" {
|
||||
return message.AttributeValue(shell)
|
||||
}
|
||||
}
|
||||
return message.AttributeValue("/bin/bash")
|
||||
},
|
||||
},
|
||||
"gecos": {
|
||||
userField: "gecos",
|
||||
notSearchable: true,
|
||||
fieldMapper: func(user *object.User) message.AttributeValue {
|
||||
// GECOS field typically contains full name and other user info
|
||||
// Format: Full Name,Room Number,Work Phone,Home Phone,Other
|
||||
gecos := user.DisplayName
|
||||
if gecos == "" {
|
||||
gecos = user.Name
|
||||
}
|
||||
return message.AttributeValue(gecos)
|
||||
},
|
||||
},
|
||||
"sshPublicKey": {
|
||||
userField: "sshPublicKey",
|
||||
notSearchable: true,
|
||||
fieldMapper: func(user *object.User) message.AttributeValue {
|
||||
// Return SSH public key from user properties
|
||||
if user.Properties != nil {
|
||||
if sshKey, ok := user.Properties["sshPublicKey"]; ok && sshKey != "" {
|
||||
return message.AttributeValue(sshKey)
|
||||
}
|
||||
}
|
||||
return message.AttributeValue("")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const ldapMemberOfAttr = "memberOf"
|
||||
|
||||
@@ -138,7 +138,7 @@ func GetOidcDiscovery(host string, applicationName string) OidcDiscovery {
|
||||
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
|
||||
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
|
||||
ResponseModesSupported: []string{"query", "fragment", "form_post"},
|
||||
GrantTypesSupported: []string{"authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"},
|
||||
GrantTypesSupported: []string{"authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},
|
||||
ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"},
|
||||
|
||||
@@ -26,6 +26,7 @@ type Order struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
UpdateTime string `xorm:"varchar(100)" json:"updateTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
// Product Info
|
||||
@@ -43,10 +44,6 @@ type Order struct {
|
||||
// Order State
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
Message string `xorm:"varchar(2000)" json:"message"`
|
||||
|
||||
// Order Duration
|
||||
StartTime string `xorm:"varchar(100)" json:"startTime"`
|
||||
EndTime string `xorm:"varchar(100)" json:"endTime"`
|
||||
}
|
||||
|
||||
type ProductInfo struct {
|
||||
@@ -138,6 +135,14 @@ func UpdateOrder(id string, order *Order) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if o.State != order.State {
|
||||
if order.State == "Created" {
|
||||
order.UpdateTime = ""
|
||||
} else {
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Equal(o.Products, order.Products) {
|
||||
existingInfos := make(map[string]ProductInfo, len(o.ProductInfos))
|
||||
for _, info := range o.ProductInfos {
|
||||
|
||||
@@ -99,8 +99,7 @@ func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User) (*Order
|
||||
Currency: orderCurrency,
|
||||
State: "Created",
|
||||
Message: "",
|
||||
StartTime: util.GetCurrentTime(),
|
||||
EndTime: "",
|
||||
UpdateTime: "",
|
||||
}
|
||||
|
||||
affected, err := AddOrder(order)
|
||||
@@ -180,6 +179,17 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
return nil, nil, fmt.Errorf("the plan: %s does not exist", productInfo.PlanName)
|
||||
}
|
||||
|
||||
// Check if plan restricts user to one subscription
|
||||
if plan.IsExclusive {
|
||||
hasSubscription, err := HasActiveSubscriptionForPlan(owner, user.Name, plan.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if hasSubscription {
|
||||
return nil, nil, fmt.Errorf("user already has an active subscription for plan: %s", plan.Name)
|
||||
}
|
||||
}
|
||||
|
||||
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -344,7 +354,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
if provider.Type == "Dummy" || provider.Type == "Balance" {
|
||||
order.State = "Paid"
|
||||
order.Message = "Payment successful"
|
||||
order.EndTime = util.GetCurrentTime()
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
// Update order state first to avoid inconsistency
|
||||
@@ -371,6 +381,6 @@ func CancelOrder(order *Order) (bool, error) {
|
||||
|
||||
order.State = "Canceled"
|
||||
order.Message = "Canceled by user"
|
||||
order.EndTime = util.GetCurrentTime()
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
return UpdateOrder(order.GetId(), order)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type AccountItem struct {
|
||||
ViewRule string `json:"viewRule"`
|
||||
ModifyRule string `json:"modifyRule"`
|
||||
Regex string `json:"regex"`
|
||||
Tab string `json:"tab"`
|
||||
}
|
||||
|
||||
type ThemeData struct {
|
||||
@@ -88,6 +89,7 @@ type Organization struct {
|
||||
|
||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||
MfaRememberInHours int `json:"mfaRememberInHours"`
|
||||
AccountMenu string `xorm:"varchar(20)" json:"accountMenu"`
|
||||
AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
|
||||
|
||||
OrgBalance float64 `json:"orgBalance"`
|
||||
|
||||
@@ -301,16 +301,19 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
|
||||
if payment.State == pp.PaymentStatePaid {
|
||||
order.State = "Paid"
|
||||
order.Message = "Payment successful"
|
||||
order.EndTime = util.GetCurrentTime()
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
} else if payment.State == pp.PaymentStateError {
|
||||
order.State = "PaymentFailed"
|
||||
order.Message = payment.Message
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
} else if payment.State == pp.PaymentStateCanceled {
|
||||
order.State = "Canceled"
|
||||
order.Message = "Payment was cancelled"
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
} else if payment.State == pp.PaymentStateTimeout {
|
||||
order.State = "Timeout"
|
||||
order.Message = "Payment timed out"
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
}
|
||||
_, err = UpdateOrder(order.GetId(), order)
|
||||
if err != nil {
|
||||
|
||||
@@ -35,6 +35,7 @@ type Plan struct {
|
||||
Product string `xorm:"varchar(100)" json:"product"`
|
||||
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
IsExclusive bool `json:"isExclusive"` // if true, a user can only have at most one subscription of this plan
|
||||
|
||||
Role string `xorm:"varchar(100)" json:"role"`
|
||||
Options []string `xorm:"-" json:"options"`
|
||||
|
||||
@@ -175,8 +175,10 @@ func DeleteSession(id, curSessionId string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If session doesn't exist, return success with no rows affected
|
||||
// This is a valid state (e.g., when a user has no active session)
|
||||
if session == nil {
|
||||
return false, fmt.Errorf("session is nil")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if slices.Contains(session.SessionId, curSessionId) {
|
||||
|
||||
@@ -28,6 +28,8 @@ func getSmsClient(provider *Provider) (sender.SmsClient, error) {
|
||||
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
|
||||
} else if provider.Type == "Custom HTTP SMS" {
|
||||
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title, provider.TemplateCode, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl, provider.EnableProxy)
|
||||
} else if provider.Type == "Alibaba Cloud PNVS SMS" {
|
||||
client, err = newPnvsSmsClient(provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.RegionId)
|
||||
} else {
|
||||
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
|
||||
}
|
||||
@@ -48,7 +50,7 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
|
||||
if provider.AppId != "" {
|
||||
phoneNumbers = append([]string{provider.AppId}, phoneNumbers...)
|
||||
}
|
||||
} else if provider.Type == sender.Aliyun {
|
||||
} else if provider.Type == sender.Aliyun || provider.Type == "Alibaba Cloud PNVS SMS" {
|
||||
for i, number := range phoneNumbers {
|
||||
phoneNumbers[i] = strings.TrimPrefix(number, "+86")
|
||||
}
|
||||
|
||||
86
object/sms_pnvs.go
Normal file
86
object/sms_pnvs.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dypnsapi"
|
||||
)
|
||||
|
||||
type PnvsSmsClient struct {
|
||||
template string
|
||||
sign string
|
||||
core *dypnsapi.Client
|
||||
}
|
||||
|
||||
func newPnvsSmsClient(accessId string, accessKey string, sign string, template string, regionId string) (*PnvsSmsClient, error) {
|
||||
if regionId == "" {
|
||||
regionId = "cn-hangzhou"
|
||||
}
|
||||
|
||||
client, err := dypnsapi.NewClientWithAccessKey(regionId, accessId, accessKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pnvsClient := &PnvsSmsClient{
|
||||
template: template,
|
||||
core: client,
|
||||
sign: sign,
|
||||
}
|
||||
|
||||
return pnvsClient, nil
|
||||
}
|
||||
|
||||
func (c *PnvsSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||
if len(targetPhoneNumber) == 0 {
|
||||
return fmt.Errorf("missing parameter: targetPhoneNumber")
|
||||
}
|
||||
|
||||
// PNVS sends to one phone number at a time
|
||||
phoneNumber := targetPhoneNumber[0]
|
||||
|
||||
request := dypnsapi.CreateSendSmsVerifyCodeRequest()
|
||||
request.Scheme = "https"
|
||||
request.PhoneNumber = phoneNumber
|
||||
request.TemplateCode = c.template
|
||||
request.SignName = c.sign
|
||||
|
||||
// TemplateParam is optional for PNVS as it can auto-generate verification codes
|
||||
// But if params are provided, we'll pass them
|
||||
if len(param) > 0 {
|
||||
templateParam, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.TemplateParam = string(templateParam)
|
||||
}
|
||||
|
||||
response, err := c.core.SendSmsVerifyCode(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.Code != "OK" {
|
||||
if response.Message != "" {
|
||||
return fmt.Errorf(response.Message)
|
||||
}
|
||||
return fmt.Errorf("PNVS SMS send failed with code: %s", response.Code)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -217,6 +217,26 @@ func GetSubscription(id string) (*Subscription, error) {
|
||||
return getSubscription(owner, name)
|
||||
}
|
||||
|
||||
func HasActiveSubscriptionForPlan(owner, userName, planName string) (bool, error) {
|
||||
subscriptions := []*Subscription{}
|
||||
err := ormer.Engine.Find(&subscriptions, &Subscription{Owner: owner, User: userName, Plan: planName})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, sub := range subscriptions {
|
||||
err = sub.UpdateState()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Check if subscription is active, upcoming, or pending (not expired, error, or suspended)
|
||||
if sub.State == SubStateActive || sub.State == SubStateUpcoming || sub.State == SubStatePending {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func UpdateSubscription(id string, subscription *Subscription) (bool, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
|
||||
@@ -370,3 +370,15 @@ func (p *ActiveDirectorySyncerProvider) adEntryToOriginalUser(entry *goldap.Entr
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Active Directory (not implemented yet)
|
||||
func (p *ActiveDirectorySyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Active Directory group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *ActiveDirectorySyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Active Directory user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -274,3 +274,15 @@ func (p *AzureAdSyncerProvider) getAzureAdOriginalUsers() ([]*OriginalUser, erro
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Azure AD (not implemented yet)
|
||||
func (p *AzureAdSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Azure AD group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *AzureAdSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Azure AD user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -60,9 +60,19 @@ func addSyncerJob(syncer *Syncer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync groups as well
|
||||
err = syncer.syncGroups()
|
||||
if err != nil {
|
||||
// Log error but don't fail the entire sync
|
||||
fmt.Printf("Warning: syncGroups() error: %s\n", err.Error())
|
||||
}
|
||||
|
||||
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
|
||||
cron := getCronMap(syncer.Name)
|
||||
_, err = cron.AddFunc(schedule, syncer.syncUsersNoError)
|
||||
_, err = cron.AddFunc(schedule, func() {
|
||||
syncer.syncUsersNoError()
|
||||
syncer.syncGroupsNoError()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -164,3 +164,15 @@ func (t dsnConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
||||
func (t dsnConnector) Driver() driver.Driver {
|
||||
return t.driver
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Database (not implemented yet)
|
||||
func (p *DatabaseSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Database group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *DatabaseSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Database user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -109,6 +109,21 @@ type DingtalkDeptListResp struct {
|
||||
RequestId string `json:"request_id"`
|
||||
}
|
||||
|
||||
type DingtalkDepartment struct {
|
||||
DeptId int64 `json:"dept_id"`
|
||||
Name string `json:"name"`
|
||||
ParentId int64 `json:"parent_id"`
|
||||
CreateDeptGroup bool `json:"create_dept_group"`
|
||||
AutoAddUser bool `json:"auto_add_user"`
|
||||
}
|
||||
|
||||
type DingtalkDeptDetailResp struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
Result *DingtalkDepartment `json:"result"`
|
||||
RequestId string `json:"request_id"`
|
||||
}
|
||||
|
||||
// getDingtalkAccessToken gets access token from DingTalk API
|
||||
func (p *DingtalkSyncerProvider) getDingtalkAccessToken() (string, error) {
|
||||
// syncer.User should be the appKey
|
||||
@@ -194,6 +209,34 @@ func (p *DingtalkSyncerProvider) getDingtalkDepartments(accessToken string) ([]i
|
||||
return deptIds, nil
|
||||
}
|
||||
|
||||
// getDingtalkDepartmentDetails gets detailed department information
|
||||
func (p *DingtalkSyncerProvider) getDingtalkDepartmentDetails(accessToken string, deptId int64) (*DingtalkDepartment, error) {
|
||||
apiUrl := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/department/get?access_token=%s",
|
||||
url.QueryEscape(accessToken))
|
||||
|
||||
postData := map[string]interface{}{
|
||||
"dept_id": deptId,
|
||||
}
|
||||
|
||||
data, err := p.postJSON(apiUrl, postData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp DingtalkDeptDetailResp
|
||||
err = json.Unmarshal(data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Errcode != 0 {
|
||||
return nil, fmt.Errorf("failed to get department details for %d: errcode=%d, errmsg=%s",
|
||||
deptId, resp.Errcode, resp.Errmsg)
|
||||
}
|
||||
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// getDingtalkUsersFromDept gets users from a specific department
|
||||
func (p *DingtalkSyncerProvider) getDingtalkUsersFromDept(accessToken string, deptId int64) ([]*DingtalkUser, error) {
|
||||
allUsers := []*DingtalkUser{}
|
||||
@@ -374,6 +417,11 @@ func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *Dingta
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Add department IDs to Groups field
|
||||
for _, deptId := range dingtalkUser.Department {
|
||||
user.Groups = append(user.Groups, fmt.Sprintf("%d", deptId))
|
||||
}
|
||||
|
||||
// Set IsForbidden based on active status (active=false means user is forbidden)
|
||||
user.IsForbidden = !dingtalkUser.Active
|
||||
|
||||
@@ -384,3 +432,73 @@ func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *Dingta
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups (departments) from DingTalk
|
||||
func (p *DingtalkSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// Get access token
|
||||
accessToken, err := p.getDingtalkAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all department IDs
|
||||
deptIds, err := p.getDingtalkDepartments(accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get detailed information for each department
|
||||
originalGroups := []*OriginalGroup{}
|
||||
for _, deptId := range deptIds {
|
||||
dept, err := p.getDingtalkDepartmentDetails(accessToken, deptId)
|
||||
if err != nil {
|
||||
// Log error but continue with other departments
|
||||
fmt.Printf("Warning: failed to get details for department %d: %v\n", deptId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
originalGroup := p.dingtalkDepartmentToOriginalGroup(dept)
|
||||
originalGroups = append(originalGroups, originalGroup)
|
||||
}
|
||||
|
||||
return originalGroups, nil
|
||||
}
|
||||
|
||||
// dingtalkDepartmentToOriginalGroup converts DingTalk department to Casdoor OriginalGroup
|
||||
func (p *DingtalkSyncerProvider) dingtalkDepartmentToOriginalGroup(dept *DingtalkDepartment) *OriginalGroup {
|
||||
// Convert department ID to string for group ID
|
||||
deptIdStr := fmt.Sprintf("%d", dept.DeptId)
|
||||
|
||||
return &OriginalGroup{
|
||||
Id: deptIdStr,
|
||||
Name: deptIdStr, // Use ID as name for uniqueness
|
||||
DisplayName: dept.Name, // Use actual name as display name
|
||||
Description: "", // DingTalk doesn't provide description
|
||||
Type: "department", // Mark as department type
|
||||
Manager: "", // DingTalk doesn't provide manager in dept details
|
||||
Email: "", // DingTalk doesn't provide email for departments
|
||||
}
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group (department) IDs that a user belongs to
|
||||
func (p *DingtalkSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// Get access token
|
||||
accessToken, err := p.getDingtalkAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get detailed user information which includes department list
|
||||
user, err := p.getDingtalkUserDetails(accessToken, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert department IDs to strings
|
||||
groupIds := []string{}
|
||||
for _, deptId := range user.Department {
|
||||
groupIds = append(groupIds, fmt.Sprintf("%d", deptId))
|
||||
}
|
||||
|
||||
return groupIds, nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ func (p *GoogleWorkspaceSyncerProvider) getAdminService() (*admin.Service, error
|
||||
PrivateKey: []byte(serviceAccount.PrivateKey),
|
||||
Scopes: []string{
|
||||
admin.AdminDirectoryUserReadonlyScope,
|
||||
admin.AdminDirectoryGroupReadonlyScope,
|
||||
},
|
||||
TokenURL: google.JWTTokenURL,
|
||||
Subject: adminEmail, // Impersonate the admin user
|
||||
@@ -202,12 +203,189 @@ func (p *GoogleWorkspaceSyncerProvider) getGoogleWorkspaceOriginalUsers() ([]*Or
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all groups and their members to build a user-to-groups mapping
|
||||
// This avoids N+1 queries by fetching group memberships upfront
|
||||
userGroupsMap, err := p.buildUserGroupsMap(service)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to fetch group memberships: %v. Users will have no groups assigned.\n", err)
|
||||
userGroupsMap = make(map[string][]string)
|
||||
}
|
||||
|
||||
// Convert Google Workspace users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, gwUser := range gwUsers {
|
||||
originalUser := p.googleWorkspaceUserToOriginalUser(gwUser)
|
||||
|
||||
// Assign groups from the pre-built map
|
||||
if groups, exists := userGroupsMap[gwUser.PrimaryEmail]; exists {
|
||||
originalUser.Groups = groups
|
||||
} else {
|
||||
originalUser.Groups = []string{}
|
||||
}
|
||||
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// buildUserGroupsMap builds a map of user email to group emails by iterating through all groups
|
||||
// and their members. This is more efficient than querying groups for each user individually.
|
||||
func (p *GoogleWorkspaceSyncerProvider) buildUserGroupsMap(service *admin.Service) (map[string][]string, error) {
|
||||
userGroupsMap := make(map[string][]string)
|
||||
|
||||
// Get all groups
|
||||
groups, err := p.getGoogleWorkspaceGroups(service)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch groups: %v", err)
|
||||
}
|
||||
|
||||
// For each group, get its members and populate the user-to-groups map
|
||||
for _, group := range groups {
|
||||
members, err := p.getGroupMembers(service, group.Id)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to get members for group %s: %v\n", group.Email, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add this group to each member's group list
|
||||
for _, member := range members {
|
||||
userGroupsMap[member.Email] = append(userGroupsMap[member.Email], group.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return userGroupsMap, nil
|
||||
}
|
||||
|
||||
// getGroupMembers retrieves all members of a specific group
|
||||
func (p *GoogleWorkspaceSyncerProvider) getGroupMembers(service *admin.Service, groupId string) ([]*admin.Member, error) {
|
||||
allMembers := []*admin.Member{}
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
call := service.Members.List(groupId).MaxResults(500)
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list members: %v", err)
|
||||
}
|
||||
|
||||
allMembers = append(allMembers, resp.Members...)
|
||||
|
||||
// Handle pagination
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
return allMembers, nil
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Google Workspace
|
||||
func (p *GoogleWorkspaceSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// Get Admin SDK service
|
||||
service, err := p.getAdminService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all groups from Google Workspace
|
||||
gwGroups, err := p.getGoogleWorkspaceGroups(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Google Workspace groups to Casdoor OriginalGroup
|
||||
originalGroups := []*OriginalGroup{}
|
||||
for _, gwGroup := range gwGroups {
|
||||
originalGroup := p.googleWorkspaceGroupToOriginalGroup(gwGroup)
|
||||
originalGroups = append(originalGroups, originalGroup)
|
||||
}
|
||||
|
||||
return originalGroups, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to
|
||||
func (p *GoogleWorkspaceSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// Get Admin SDK service
|
||||
service, err := p.getAdminService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get groups for the user
|
||||
groupIds := []string{}
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
call := service.Groups.List().UserKey(userId)
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list user groups: %v", err)
|
||||
}
|
||||
|
||||
for _, group := range resp.Groups {
|
||||
groupIds = append(groupIds, group.Email)
|
||||
}
|
||||
|
||||
// Handle pagination
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
return groupIds, nil
|
||||
}
|
||||
|
||||
// getGoogleWorkspaceGroups gets all groups from Google Workspace using Admin SDK API
|
||||
func (p *GoogleWorkspaceSyncerProvider) getGoogleWorkspaceGroups(service *admin.Service) ([]*admin.Group, error) {
|
||||
allGroups := []*admin.Group{}
|
||||
pageToken := ""
|
||||
|
||||
// Get the customer ID (use "my_customer" for the domain)
|
||||
customer := "my_customer"
|
||||
|
||||
for {
|
||||
call := service.Groups.List().Customer(customer).MaxResults(500)
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list groups: %v", err)
|
||||
}
|
||||
|
||||
allGroups = append(allGroups, resp.Groups...)
|
||||
|
||||
// Handle pagination
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
return allGroups, nil
|
||||
}
|
||||
|
||||
// googleWorkspaceGroupToOriginalGroup converts Google Workspace group to Casdoor OriginalGroup
|
||||
func (p *GoogleWorkspaceSyncerProvider) googleWorkspaceGroupToOriginalGroup(gwGroup *admin.Group) *OriginalGroup {
|
||||
group := &OriginalGroup{
|
||||
Id: gwGroup.Id,
|
||||
Name: gwGroup.Email,
|
||||
DisplayName: gwGroup.Name,
|
||||
Description: gwGroup.Description,
|
||||
Email: gwGroup.Email,
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
204
object/syncer_googleworkspace_test.go
Normal file
204
object/syncer_googleworkspace_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
)
|
||||
|
||||
func TestGoogleWorkspaceUserToOriginalUser(t *testing.T) {
|
||||
provider := &GoogleWorkspaceSyncerProvider{
|
||||
Syncer: &Syncer{},
|
||||
}
|
||||
|
||||
// Test case 1: Full Google Workspace user with all fields
|
||||
gwUser := &admin.User{
|
||||
Id: "user-123",
|
||||
PrimaryEmail: "john.doe@example.com",
|
||||
Name: &admin.UserName{
|
||||
FullName: "John Doe",
|
||||
GivenName: "John",
|
||||
FamilyName: "Doe",
|
||||
},
|
||||
ThumbnailPhotoUrl: "https://example.com/avatar.jpg",
|
||||
Suspended: false,
|
||||
IsAdmin: true,
|
||||
CreationTime: "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
originalUser := provider.googleWorkspaceUserToOriginalUser(gwUser)
|
||||
|
||||
// Verify basic fields
|
||||
if originalUser.Id != "user-123" {
|
||||
t.Errorf("Expected Id to be 'user-123', got '%s'", originalUser.Id)
|
||||
}
|
||||
if originalUser.Name != "john.doe@example.com" {
|
||||
t.Errorf("Expected Name to be 'john.doe@example.com', got '%s'", originalUser.Name)
|
||||
}
|
||||
if originalUser.Email != "john.doe@example.com" {
|
||||
t.Errorf("Expected Email to be 'john.doe@example.com', got '%s'", originalUser.Email)
|
||||
}
|
||||
if originalUser.DisplayName != "John Doe" {
|
||||
t.Errorf("Expected DisplayName to be 'John Doe', got '%s'", originalUser.DisplayName)
|
||||
}
|
||||
if originalUser.FirstName != "John" {
|
||||
t.Errorf("Expected FirstName to be 'John', got '%s'", originalUser.FirstName)
|
||||
}
|
||||
if originalUser.LastName != "Doe" {
|
||||
t.Errorf("Expected LastName to be 'Doe', got '%s'", originalUser.LastName)
|
||||
}
|
||||
if originalUser.Avatar != "https://example.com/avatar.jpg" {
|
||||
t.Errorf("Expected Avatar to be 'https://example.com/avatar.jpg', got '%s'", originalUser.Avatar)
|
||||
}
|
||||
if originalUser.IsForbidden != false {
|
||||
t.Errorf("Expected IsForbidden to be false for non-suspended user, got %v", originalUser.IsForbidden)
|
||||
}
|
||||
if originalUser.IsAdmin != true {
|
||||
t.Errorf("Expected IsAdmin to be true, got %v", originalUser.IsAdmin)
|
||||
}
|
||||
|
||||
// Test case 2: Suspended Google Workspace user
|
||||
suspendedUser := &admin.User{
|
||||
Id: "user-456",
|
||||
PrimaryEmail: "jane.doe@example.com",
|
||||
Name: &admin.UserName{
|
||||
FullName: "Jane Doe",
|
||||
},
|
||||
Suspended: true,
|
||||
}
|
||||
|
||||
suspendedOriginalUser := provider.googleWorkspaceUserToOriginalUser(suspendedUser)
|
||||
if suspendedOriginalUser.IsForbidden != true {
|
||||
t.Errorf("Expected IsForbidden to be true for suspended user, got %v", suspendedOriginalUser.IsForbidden)
|
||||
}
|
||||
|
||||
// Test case 3: User with no Name object (should not panic)
|
||||
minimalUser := &admin.User{
|
||||
Id: "user-789",
|
||||
PrimaryEmail: "bob@example.com",
|
||||
}
|
||||
|
||||
minimalOriginalUser := provider.googleWorkspaceUserToOriginalUser(minimalUser)
|
||||
if minimalOriginalUser.DisplayName != "" {
|
||||
t.Errorf("Expected DisplayName to be empty for minimal user, got '%s'", minimalOriginalUser.DisplayName)
|
||||
}
|
||||
|
||||
// Test case 4: Display name construction from first/last name when FullName is empty
|
||||
noFullNameUser := &admin.User{
|
||||
Id: "user-101",
|
||||
PrimaryEmail: "alice@example.com",
|
||||
Name: &admin.UserName{
|
||||
GivenName: "Alice",
|
||||
FamilyName: "Jones",
|
||||
},
|
||||
}
|
||||
|
||||
noFullNameOriginalUser := provider.googleWorkspaceUserToOriginalUser(noFullNameUser)
|
||||
if noFullNameOriginalUser.DisplayName != "Alice Jones" {
|
||||
t.Errorf("Expected DisplayName to be constructed as 'Alice Jones', got '%s'", noFullNameOriginalUser.DisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleWorkspaceGroupToOriginalGroup(t *testing.T) {
|
||||
provider := &GoogleWorkspaceSyncerProvider{
|
||||
Syncer: &Syncer{},
|
||||
}
|
||||
|
||||
// Test case 1: Full Google Workspace group with all fields
|
||||
gwGroup := &admin.Group{
|
||||
Id: "group-123",
|
||||
Email: "team@example.com",
|
||||
Name: "Engineering Team",
|
||||
Description: "All engineering staff",
|
||||
}
|
||||
|
||||
originalGroup := provider.googleWorkspaceGroupToOriginalGroup(gwGroup)
|
||||
|
||||
// Verify all fields
|
||||
if originalGroup.Id != "group-123" {
|
||||
t.Errorf("Expected Id to be 'group-123', got '%s'", originalGroup.Id)
|
||||
}
|
||||
if originalGroup.Name != "team@example.com" {
|
||||
t.Errorf("Expected Name to be 'team@example.com', got '%s'", originalGroup.Name)
|
||||
}
|
||||
if originalGroup.DisplayName != "Engineering Team" {
|
||||
t.Errorf("Expected DisplayName to be 'Engineering Team', got '%s'", originalGroup.DisplayName)
|
||||
}
|
||||
if originalGroup.Description != "All engineering staff" {
|
||||
t.Errorf("Expected Description to be 'All engineering staff', got '%s'", originalGroup.Description)
|
||||
}
|
||||
if originalGroup.Email != "team@example.com" {
|
||||
t.Errorf("Expected Email to be 'team@example.com', got '%s'", originalGroup.Email)
|
||||
}
|
||||
|
||||
// Test case 2: Minimal group
|
||||
minimalGroup := &admin.Group{
|
||||
Id: "group-456",
|
||||
Email: "minimal@example.com",
|
||||
}
|
||||
|
||||
minimalOriginalGroup := provider.googleWorkspaceGroupToOriginalGroup(minimalGroup)
|
||||
if minimalOriginalGroup.DisplayName != "" {
|
||||
t.Errorf("Expected DisplayName to be empty for minimal group, got '%s'", minimalOriginalGroup.DisplayName)
|
||||
}
|
||||
if minimalOriginalGroup.Description != "" {
|
||||
t.Errorf("Expected Description to be empty for minimal group, got '%s'", minimalOriginalGroup.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSyncerProviderGoogleWorkspace(t *testing.T) {
|
||||
syncer := &Syncer{
|
||||
Type: "Google Workspace",
|
||||
Host: "admin@example.com",
|
||||
}
|
||||
|
||||
provider := GetSyncerProvider(syncer)
|
||||
|
||||
if _, ok := provider.(*GoogleWorkspaceSyncerProvider); !ok {
|
||||
t.Errorf("Expected GoogleWorkspaceSyncerProvider for type 'Google Workspace', got %T", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleWorkspaceSyncerProviderEmptyMethods(t *testing.T) {
|
||||
provider := &GoogleWorkspaceSyncerProvider{
|
||||
Syncer: &Syncer{},
|
||||
}
|
||||
|
||||
// Test AddUser returns error
|
||||
_, err := provider.AddUser(&OriginalUser{})
|
||||
if err == nil {
|
||||
t.Error("Expected AddUser to return error for read-only syncer")
|
||||
}
|
||||
|
||||
// Test UpdateUser returns error
|
||||
_, err = provider.UpdateUser(&OriginalUser{})
|
||||
if err == nil {
|
||||
t.Error("Expected UpdateUser to return error for read-only syncer")
|
||||
}
|
||||
|
||||
// Test Close returns no error
|
||||
err = provider.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Expected Close to return nil, got error: %v", err)
|
||||
}
|
||||
|
||||
// Test InitAdapter returns no error
|
||||
err = provider.InitAdapter()
|
||||
if err != nil {
|
||||
t.Errorf("Expected InitAdapter to return nil, got error: %v", err)
|
||||
}
|
||||
}
|
||||
121
object/syncer_group.go
Normal file
121
object/syncer_group.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (syncer *Syncer) getOriginalGroups() ([]*OriginalGroup, error) {
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.GetOriginalGroups()
|
||||
}
|
||||
|
||||
func (syncer *Syncer) createGroupFromOriginalGroup(originalGroup *OriginalGroup) *Group {
|
||||
group := &Group{
|
||||
Owner: syncer.Organization,
|
||||
Name: originalGroup.Name,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
UpdatedTime: util.GetCurrentTime(),
|
||||
DisplayName: originalGroup.DisplayName,
|
||||
Type: originalGroup.Type,
|
||||
Manager: originalGroup.Manager,
|
||||
IsEnabled: true,
|
||||
IsTopGroup: true,
|
||||
}
|
||||
|
||||
if originalGroup.Email != "" {
|
||||
group.ContactEmail = originalGroup.Email
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
func (syncer *Syncer) syncGroups() error {
|
||||
fmt.Printf("Running syncGroups()..\n")
|
||||
|
||||
// Get existing groups from Casdoor
|
||||
groups, err := GetGroups(syncer.Organization)
|
||||
if err != nil {
|
||||
line := fmt.Sprintf("[%s] %s\n", util.GetCurrentTime(), err.Error())
|
||||
_, err2 := updateSyncerErrorText(syncer, line)
|
||||
if err2 != nil {
|
||||
panic(err2)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get groups from the external system
|
||||
oGroups, err := syncer.getOriginalGroups()
|
||||
if err != nil {
|
||||
line := fmt.Sprintf("[%s] %s\n", util.GetCurrentTime(), err.Error())
|
||||
_, err2 := updateSyncerErrorText(syncer, line)
|
||||
if err2 != nil {
|
||||
panic(err2)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Groups: %d, oGroups: %d\n", len(groups), len(oGroups))
|
||||
|
||||
// Create a map of existing groups by name
|
||||
myGroups := map[string]*Group{}
|
||||
for _, group := range groups {
|
||||
myGroups[group.Name] = group
|
||||
}
|
||||
|
||||
// Sync groups from external system to Casdoor
|
||||
newGroups := []*Group{}
|
||||
for _, oGroup := range oGroups {
|
||||
if _, ok := myGroups[oGroup.Name]; !ok {
|
||||
newGroup := syncer.createGroupFromOriginalGroup(oGroup)
|
||||
fmt.Printf("New group: %v\n", newGroup)
|
||||
newGroups = append(newGroups, newGroup)
|
||||
} else {
|
||||
// Group already exists, could update it here if needed
|
||||
existingGroup := myGroups[oGroup.Name]
|
||||
|
||||
// Update group display name and other fields if they've changed
|
||||
if existingGroup.DisplayName != oGroup.DisplayName {
|
||||
existingGroup.DisplayName = oGroup.DisplayName
|
||||
existingGroup.UpdatedTime = util.GetCurrentTime()
|
||||
_, err = UpdateGroup(existingGroup.GetId(), existingGroup)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update group %s: %v\n", existingGroup.Name, err)
|
||||
} else {
|
||||
fmt.Printf("Updated group: %s\n", existingGroup.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(newGroups) != 0 {
|
||||
_, err = AddGroupsInBatch(newGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (syncer *Syncer) syncGroupsNoError() {
|
||||
err := syncer.syncGroups()
|
||||
if err != nil {
|
||||
fmt.Printf("syncGroupsNoError() error: %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,17 @@
|
||||
|
||||
package object
|
||||
|
||||
// OriginalGroup represents a group from an external system
|
||||
type OriginalGroup struct {
|
||||
Id string
|
||||
Name string
|
||||
DisplayName string
|
||||
Description string
|
||||
Type string
|
||||
Manager string
|
||||
Email string
|
||||
}
|
||||
|
||||
// SyncerProvider defines the interface that all syncer implementations must satisfy.
|
||||
// Different syncer types (Database, Keycloak, WeCom, Azure AD) implement this interface.
|
||||
type SyncerProvider interface {
|
||||
@@ -23,6 +34,12 @@ type SyncerProvider interface {
|
||||
// GetOriginalUsers retrieves all users from the external system
|
||||
GetOriginalUsers() ([]*OriginalUser, error)
|
||||
|
||||
// GetOriginalGroups retrieves all groups from the external system
|
||||
GetOriginalGroups() ([]*OriginalGroup, error)
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to
|
||||
GetOriginalUserGroups(userId string) ([]string, error)
|
||||
|
||||
// AddUser adds a new user to the external system
|
||||
AddUser(user *OriginalUser) (bool, error)
|
||||
|
||||
|
||||
@@ -29,3 +29,15 @@ func (p *KeycloakSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
|
||||
// Note: Keycloak-specific user mapping is handled in syncer_util.go
|
||||
// via getOriginalUsersFromMap which checks syncer.Type == "Keycloak"
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Keycloak (not implemented yet)
|
||||
func (p *KeycloakSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Keycloak group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *KeycloakSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Keycloak user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -414,3 +414,15 @@ func (p *LarkSyncerProvider) larkUserToOriginalUser(larkUser *LarkUser) *Origina
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Lark (not implemented yet)
|
||||
func (p *LarkSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Lark group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *LarkSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Lark user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -296,3 +296,15 @@ func (p *OktaSyncerProvider) getOktaOriginalUsers() ([]*OriginalUser, error) {
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Okta (not implemented yet)
|
||||
func (p *OktaSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Okta group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *OktaSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Okta user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -335,3 +335,15 @@ func (p *SCIMSyncerProvider) scimUserToOriginalUser(scimUser *SCIMUser) *Origina
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from SCIM (not implemented yet)
|
||||
func (p *SCIMSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement SCIM group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *SCIMSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement SCIM user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -303,3 +303,15 @@ func (p *WecomSyncerProvider) wecomUserToOriginalUser(wecomUser *WecomUser) *Ori
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from WeCom (not implemented yet)
|
||||
func (p *WecomSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement WeCom group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *WecomSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement WeCom user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -209,7 +210,7 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string) (interface{}, error) {
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, audience string) (interface{}, error) {
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -244,6 +245,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:device_code":
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:token-exchange": // Token Exchange Grant (RFC 8693)
|
||||
token, tokenError, err = GetTokenExchangeToken(application, clientSecret, subjectToken, subjectTokenType, audience, scope, host)
|
||||
case "refresh_token":
|
||||
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
if err != nil {
|
||||
@@ -963,6 +966,183 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetTokenExchangeToken
|
||||
// Token Exchange Grant (RFC 8693)
|
||||
// Exchanges a subject token for a new token with different audience or scope
|
||||
func GetTokenExchangeToken(application *Application, clientSecret string, subjectToken string, subjectTokenType string, audience string, scope string, host string) (*Token, *TokenError, error) {
|
||||
// Verify client secret
|
||||
if application.ClientSecret != clientSecret {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate subject_token parameter
|
||||
if subjectToken == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidRequest,
|
||||
ErrorDescription: "subject_token is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate subject_token_type parameter
|
||||
// RFC 8693 defines standard token type identifiers
|
||||
if subjectTokenType == "" {
|
||||
subjectTokenType = "urn:ietf:params:oauth:token-type:access_token" // Default to access_token
|
||||
}
|
||||
|
||||
// Support common token types
|
||||
supportedTokenTypes := []string{
|
||||
"urn:ietf:params:oauth:token-type:access_token",
|
||||
"urn:ietf:params:oauth:token-type:jwt",
|
||||
"urn:ietf:params:oauth:token-type:id_token",
|
||||
}
|
||||
|
||||
isValidTokenType := false
|
||||
for _, tokenType := range supportedTokenTypes {
|
||||
if subjectTokenType == tokenType {
|
||||
isValidTokenType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidTokenType {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidRequest,
|
||||
ErrorDescription: fmt.Sprintf("unsupported subject_token_type: %s", subjectTokenType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get certificate for token validation
|
||||
cert, err := getCertByApplication(application)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("cert: %s cannot be found", application.Cert),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Parse and validate the subject token
|
||||
var subjectOwner, subjectName, subjectScope string
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
standardClaims, err := ParseStandardJwtToken(subjectToken, cert)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
subjectOwner = standardClaims.Owner
|
||||
subjectName = standardClaims.Name
|
||||
subjectScope = standardClaims.Scope
|
||||
} else {
|
||||
claims, err := ParseJwtToken(subjectToken, cert)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
subjectOwner = claims.Owner
|
||||
subjectName = claims.Name
|
||||
subjectScope = claims.Scope
|
||||
}
|
||||
|
||||
// Get the user from the subject token
|
||||
user, err := getUser(subjectOwner, subjectName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("user from subject_token does not exist: %s", util.GetId(subjectOwner, subjectName)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if user.IsForbidden {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Handle scope parameter
|
||||
// If scope is not provided, use the scope from the subject token
|
||||
// If scope is provided, it should be a subset of the subject token's scope (downscoping)
|
||||
if scope == "" {
|
||||
scope = subjectScope
|
||||
} else {
|
||||
// Validate scope downscoping (basic implementation)
|
||||
// In a production environment, you would implement more sophisticated scope validation
|
||||
if subjectScope != "" {
|
||||
subjectScopes := strings.Split(subjectScope, " ")
|
||||
requestedScopes := strings.Split(scope, " ")
|
||||
for _, requestedScope := range requestedScopes {
|
||||
if requestedScope == "" {
|
||||
continue // Skip empty strings
|
||||
}
|
||||
found := false
|
||||
for _, existingScope := range subjectScopes {
|
||||
if existingScope != "" && requestedScope == existingScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidScope,
|
||||
ErrorDescription: fmt.Sprintf("requested scope '%s' is not in subject token's scope", requestedScope),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extend user with roles and permissions
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate new JWT token
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create token object
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int(application.ExpireInHours * float64(hourSeconds)),
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
func GetAccessTokenByUser(user *User, host string) (string, error) {
|
||||
application, err := GetApplicationByUser(user)
|
||||
if err != nil {
|
||||
|
||||
@@ -689,6 +689,15 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
|
||||
if user.OriginalRefreshToken != "" {
|
||||
user.OriginalRefreshToken = "***"
|
||||
}
|
||||
// Mask per-provider OAuth tokens in Properties
|
||||
if user.Properties != nil {
|
||||
for key := range user.Properties {
|
||||
// More specific pattern matching to avoid masking unrelated properties
|
||||
if strings.HasPrefix(key, "oauth_") && (strings.HasSuffix(key, "_accessToken") || strings.HasSuffix(key, "_refreshToken")) {
|
||||
user.Properties[key] = "***"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user.ManagedAccounts != nil {
|
||||
|
||||
@@ -184,9 +184,32 @@ func getUserExtraProperty(user *User, providerType, key string) (string, error)
|
||||
return extra[key], nil
|
||||
}
|
||||
|
||||
// getOAuthTokenPropertyKey returns the property key for storing OAuth tokens
|
||||
func getOAuthTokenPropertyKey(providerType string, tokenType string) string {
|
||||
return fmt.Sprintf("oauth_%s_%s", providerType, tokenType)
|
||||
}
|
||||
|
||||
// GetUserOAuthAccessToken retrieves the OAuth access token for a specific provider
|
||||
func GetUserOAuthAccessToken(user *User, providerType string) string {
|
||||
return getUserProperty(user, getOAuthTokenPropertyKey(providerType, "accessToken"))
|
||||
}
|
||||
|
||||
// GetUserOAuthRefreshToken retrieves the OAuth refresh token for a specific provider
|
||||
func GetUserOAuthRefreshToken(user *User, providerType string) string {
|
||||
return getUserProperty(user, getOAuthTokenPropertyKey(providerType, "refreshToken"))
|
||||
}
|
||||
|
||||
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo, token *oauth2.Token, userMapping ...map[string]string) (bool, error) {
|
||||
// Store the original OAuth provider token if available
|
||||
if token != nil && token.AccessToken != "" {
|
||||
// Store tokens per provider in Properties map
|
||||
setUserProperty(user, getOAuthTokenPropertyKey(providerType, "accessToken"), token.AccessToken)
|
||||
|
||||
if token.RefreshToken != "" {
|
||||
setUserProperty(user, getOAuthTokenPropertyKey(providerType, "refreshToken"), token.RefreshToken)
|
||||
}
|
||||
|
||||
// Also update the legacy fields for backward compatibility
|
||||
user.OriginalToken = token.AccessToken
|
||||
user.OriginalRefreshToken = token.RefreshToken
|
||||
}
|
||||
@@ -918,6 +941,8 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
|
||||
err = setReflectAttr[[]MfaAccount](&fv, v)
|
||||
case reflect.TypeOf([]webauthn.Credential{}):
|
||||
err = setReflectAttr[[]webauthn.Credential](&fv, v)
|
||||
case reflect.TypeOf(map[string]string{}):
|
||||
err = setReflectAttr[map[string]string](&fv, v)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -850,6 +850,69 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/add-ticket": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Ticket API"
|
||||
],
|
||||
"description": "add ticket",
|
||||
"operationId": "ApiController.AddTicket",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "The details of the ticket",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/object.Ticket"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/add-ticket-message": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Ticket API"
|
||||
],
|
||||
"description": "add a message to a ticket",
|
||||
"operationId": "ApiController.AddTicketMessage",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id ( owner/name ) of the ticket",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "The message to add",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/object.TicketMessage"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/add-token": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1707,6 +1770,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/delete-ticket": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Ticket API"
|
||||
],
|
||||
"description": "delete ticket",
|
||||
"operationId": "ApiController.DeleteTicket",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "The details of the ticket",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/object.Ticket"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/delete-token": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1891,6 +1982,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/exit-impersonation-user": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"User API"
|
||||
],
|
||||
"description": "clear impersonation info for current session",
|
||||
"operationId": "ApiController.ExitImpersonateUser",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/faceid-signin-begin": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1996,6 +2104,81 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/get-all-actions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Enforcer API"
|
||||
],
|
||||
"description": "Get all actions for a user (Casbin API)",
|
||||
"operationId": "ApiController.GetAllActions",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "userId",
|
||||
"description": "user id like built-in/admin",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/get-all-objects": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Enforcer API"
|
||||
],
|
||||
"description": "Get all objects for a user (Casbin API)",
|
||||
"operationId": "ApiController.GetAllObjects",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "userId",
|
||||
"description": "user id like built-in/admin",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/get-all-roles": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Enforcer API"
|
||||
],
|
||||
"description": "Get all roles for a user (Casbin API)",
|
||||
"operationId": "ApiController.GetAllRoles",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "userId",
|
||||
"description": "user id like built-in/admin",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/get-app-login": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -3843,6 +4026,61 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/get-ticket": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Ticket API"
|
||||
],
|
||||
"description": "get ticket",
|
||||
"operationId": "ApiController.GetTicket",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id ( owner/name ) of the ticket",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/object.Ticket"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/get-tickets": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Ticket API"
|
||||
],
|
||||
"description": "get tickets",
|
||||
"operationId": "ApiController.GetTickets",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "owner",
|
||||
"description": "The owner of tickets",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/object.Ticket"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/get-token": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -4299,6 +4537,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/impersonation-user": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"User API"
|
||||
],
|
||||
"description": "set impersonation user for current admin session",
|
||||
"operationId": "ApiController.ImpersonateUser",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "formData",
|
||||
"name": "username",
|
||||
"description": "The username to impersonate (owner/name)",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/invoice-payment": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -5219,8 +5483,16 @@
|
||||
"tags": [
|
||||
"Login API"
|
||||
],
|
||||
"description": "logout the current user from all applications",
|
||||
"description": "logout the current user from all applications or current session only",
|
||||
"operationId": "ApiController.SsoLogout",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "logoutAll",
|
||||
"description": "Whether to logout from all sessions. Accepted values: 'true', '1', or empty (default: true). Any other value means false.",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
@@ -5234,8 +5506,16 @@
|
||||
"tags": [
|
||||
"Login API"
|
||||
],
|
||||
"description": "logout the current user from all applications",
|
||||
"description": "logout the current user from all applications or current session only",
|
||||
"operationId": "ApiController.SsoLogout",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "logoutAll",
|
||||
"description": "Whether to logout from all sessions. Accepted values: 'true', '1', or empty (default: true). Any other value means false.",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
@@ -6082,6 +6362,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/update-ticket": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Ticket API"
|
||||
],
|
||||
"description": "update ticket",
|
||||
"operationId": "ApiController.UpdateTicket",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id ( owner/name ) of the ticket",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "The details of the ticket",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/object.Ticket"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/update-token": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -6564,14 +6879,18 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"217748.\u003cnil\u003e.string": {
|
||||
"232967.\u003cnil\u003e.string": {
|
||||
"title": "string",
|
||||
"type": "object"
|
||||
},
|
||||
"217806.string.string": {
|
||||
"233025.string.string": {
|
||||
"title": "string",
|
||||
"type": "object"
|
||||
},
|
||||
"McpResponse": {
|
||||
"title": "McpResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"Response": {
|
||||
"title": "Response",
|
||||
"type": "object"
|
||||
@@ -6763,6 +7082,9 @@
|
||||
"regex": {
|
||||
"type": "string"
|
||||
},
|
||||
"tab": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewRule": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6814,6 +7136,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.Address": {
|
||||
"title": "Address",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"line1": {
|
||||
"type": "string"
|
||||
},
|
||||
"line2": {
|
||||
"type": "string"
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"zipCode": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.Application": {
|
||||
"title": "Application",
|
||||
"type": "object",
|
||||
@@ -6837,6 +7186,10 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"cookieExpireInHours": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"createdTime": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6870,6 +7223,9 @@
|
||||
"enablePassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableSamlAssertionSignature": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableSamlC14n10": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -7822,9 +8178,6 @@
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7837,18 +8190,15 @@
|
||||
"payment": {
|
||||
"type": "string"
|
||||
},
|
||||
"planName": {
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"pricingName": {
|
||||
"type": "string"
|
||||
},
|
||||
"productName": {
|
||||
"type": "string"
|
||||
"productInfos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/object.ProductInfo"
|
||||
}
|
||||
},
|
||||
"products": {
|
||||
"type": "array",
|
||||
@@ -7856,10 +8206,10 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"startTime": {
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"updateTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
@@ -7877,6 +8227,9 @@
|
||||
"$ref": "#/definitions/object.AccountItem"
|
||||
}
|
||||
},
|
||||
"accountMenu": {
|
||||
"type": "string"
|
||||
},
|
||||
"balanceCredit": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
@@ -8090,9 +8443,6 @@
|
||||
"invoiceUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"isRecharge": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8102,6 +8452,9 @@
|
||||
"order": {
|
||||
"type": "string"
|
||||
},
|
||||
"orderObj": {
|
||||
"$ref": "#/definitions/object.Order"
|
||||
},
|
||||
"outOrderId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8127,10 +8480,13 @@
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"productDisplayName": {
|
||||
"type": "string"
|
||||
"products": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"productName": {
|
||||
"productsDisplayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
@@ -8142,9 +8498,6 @@
|
||||
"successUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8402,6 +8755,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.ProductInfo": {
|
||||
"title": "ProductInfo",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"detail": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"type": "string"
|
||||
},
|
||||
"isRecharge": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"planName": {
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"pricingName": {
|
||||
"type": "string"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.PrometheusInfo": {
|
||||
"title": "PrometheusInfo",
|
||||
"type": "object",
|
||||
@@ -8482,6 +8876,9 @@
|
||||
"emailRegex": {
|
||||
"type": "string"
|
||||
},
|
||||
"enablePkce": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableProxy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -9024,6 +9421,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.Ticket": {
|
||||
"title": "Ticket",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/object.TicketMessage"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.TicketMessage": {
|
||||
"title": "TicketMessage",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"isAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.Token": {
|
||||
"title": "Token",
|
||||
"type": "object",
|
||||
@@ -9132,7 +9586,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/object.TransactionCategory"
|
||||
},
|
||||
"createdTime": {
|
||||
"type": "string"
|
||||
@@ -9159,7 +9613,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/definitions/pp.PaymentState"
|
||||
"type": "string"
|
||||
},
|
||||
"subtype": {
|
||||
"type": "string"
|
||||
@@ -9175,6 +9629,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.TransactionCategory": {
|
||||
"title": "TransactionCategory",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"TransactionCategoryPurchase = \"Purchase\"",
|
||||
"TransactionCategoryRecharge = \"Recharge\""
|
||||
],
|
||||
"example": "Purchase"
|
||||
},
|
||||
"object.User": {
|
||||
"title": "User",
|
||||
"type": "object",
|
||||
@@ -9194,6 +9657,12 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"addresses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/object.Address"
|
||||
}
|
||||
},
|
||||
"adfs": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9256,6 +9725,12 @@
|
||||
"box": {
|
||||
"type": "string"
|
||||
},
|
||||
"cart": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/object.ProductInfo"
|
||||
}
|
||||
},
|
||||
"casdoor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9569,10 +10044,10 @@
|
||||
"onedrive": {
|
||||
"type": "string"
|
||||
},
|
||||
"originalToken": {
|
||||
"originalRefreshToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"originalRefreshToken": {
|
||||
"originalToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"oura": {
|
||||
@@ -9828,7 +10303,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aliases": {
|
||||
"$ref": "#/definitions/217748.\u003cnil\u003e.string"
|
||||
"$ref": "#/definitions/232967.\u003cnil\u003e.string"
|
||||
},
|
||||
"links": {
|
||||
"type": "array",
|
||||
@@ -9837,7 +10312,7 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/217806.string.string"
|
||||
"$ref": "#/definitions/233025.string.string"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
|
||||
@@ -548,6 +548,47 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-ticket:
|
||||
post:
|
||||
tags:
|
||||
- Ticket API
|
||||
description: add ticket
|
||||
operationId: ApiController.AddTicket
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the ticket
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Ticket'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-ticket-message:
|
||||
post:
|
||||
tags:
|
||||
- Ticket API
|
||||
description: add a message to a ticket
|
||||
operationId: ApiController.AddTicketMessage
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the ticket
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The message to add
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.TicketMessage'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-token:
|
||||
post:
|
||||
tags:
|
||||
@@ -1099,6 +1140,24 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-ticket:
|
||||
post:
|
||||
tags:
|
||||
- Ticket API
|
||||
description: delete ticket
|
||||
operationId: ApiController.DeleteTicket
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the ticket
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Ticket'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-token:
|
||||
post:
|
||||
tags:
|
||||
@@ -1218,6 +1277,17 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/exit-impersonation-user:
|
||||
post:
|
||||
tags:
|
||||
- User API
|
||||
description: clear impersonation info for current session
|
||||
operationId: ApiController.ExitImpersonateUser
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/faceid-signin-begin:
|
||||
get:
|
||||
tags:
|
||||
@@ -1287,6 +1357,54 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Adapter'
|
||||
/api/get-all-actions:
|
||||
get:
|
||||
tags:
|
||||
- Enforcer API
|
||||
description: Get all actions for a user (Casbin API)
|
||||
operationId: ApiController.GetAllActions
|
||||
parameters:
|
||||
- in: query
|
||||
name: userId
|
||||
description: user id like built-in/admin
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/get-all-objects:
|
||||
get:
|
||||
tags:
|
||||
- Enforcer API
|
||||
description: Get all objects for a user (Casbin API)
|
||||
operationId: ApiController.GetAllObjects
|
||||
parameters:
|
||||
- in: query
|
||||
name: userId
|
||||
description: user id like built-in/admin
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/get-all-roles:
|
||||
get:
|
||||
tags:
|
||||
- Enforcer API
|
||||
description: Get all roles for a user (Casbin API)
|
||||
operationId: ApiController.GetAllRoles
|
||||
parameters:
|
||||
- in: query
|
||||
name: userId
|
||||
description: user id like built-in/admin
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/get-app-login:
|
||||
get:
|
||||
tags:
|
||||
@@ -2498,6 +2616,42 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/util.SystemInfo'
|
||||
/api/get-ticket:
|
||||
get:
|
||||
tags:
|
||||
- Ticket API
|
||||
description: get ticket
|
||||
operationId: ApiController.GetTicket
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the ticket
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Ticket'
|
||||
/api/get-tickets:
|
||||
get:
|
||||
tags:
|
||||
- Ticket API
|
||||
description: get tickets
|
||||
operationId: ApiController.GetTickets
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of tickets
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Ticket'
|
||||
/api/get-token:
|
||||
get:
|
||||
tags:
|
||||
@@ -2797,6 +2951,23 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/impersonation-user:
|
||||
post:
|
||||
tags:
|
||||
- User API
|
||||
description: set impersonation user for current admin session
|
||||
operationId: ApiController.ImpersonateUser
|
||||
parameters:
|
||||
- in: formData
|
||||
name: username
|
||||
description: The username to impersonate (owner/name)
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/invoice-payment:
|
||||
post:
|
||||
tags:
|
||||
@@ -3406,8 +3577,13 @@ paths:
|
||||
get:
|
||||
tags:
|
||||
- Login API
|
||||
description: logout the current user from all applications
|
||||
description: logout the current user from all applications or current session only
|
||||
operationId: ApiController.SsoLogout
|
||||
parameters:
|
||||
- in: query
|
||||
name: logoutAll
|
||||
description: 'Whether to logout from all sessions. Accepted values: ''true'', ''1'', or empty (default: true). Any other value means false.'
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
@@ -3416,8 +3592,13 @@ paths:
|
||||
post:
|
||||
tags:
|
||||
- Login API
|
||||
description: logout the current user from all applications
|
||||
description: logout the current user from all applications or current session only
|
||||
operationId: ApiController.SsoLogout
|
||||
parameters:
|
||||
- in: query
|
||||
name: logoutAll
|
||||
description: 'Whether to logout from all sessions. Accepted values: ''true'', ''1'', or empty (default: true). Any other value means false.'
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
@@ -3971,6 +4152,29 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-ticket:
|
||||
post:
|
||||
tags:
|
||||
- Ticket API
|
||||
description: update ticket
|
||||
operationId: ApiController.UpdateTicket
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the ticket
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the ticket
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Ticket'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-token:
|
||||
post:
|
||||
tags:
|
||||
@@ -4286,12 +4490,15 @@ paths:
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
definitions:
|
||||
217748.<nil>.string:
|
||||
232967.<nil>.string:
|
||||
title: string
|
||||
type: object
|
||||
217806.string.string:
|
||||
233025.string.string:
|
||||
title: string
|
||||
type: object
|
||||
McpResponse:
|
||||
title: McpResponse
|
||||
type: object
|
||||
Response:
|
||||
title: Response
|
||||
type: object
|
||||
@@ -4423,6 +4630,8 @@ definitions:
|
||||
type: string
|
||||
regex:
|
||||
type: string
|
||||
tab:
|
||||
type: string
|
||||
viewRule:
|
||||
type: string
|
||||
visible:
|
||||
@@ -4456,6 +4665,24 @@ definitions:
|
||||
type: boolean
|
||||
user:
|
||||
type: string
|
||||
object.Address:
|
||||
title: Address
|
||||
type: object
|
||||
properties:
|
||||
city:
|
||||
type: string
|
||||
line1:
|
||||
type: string
|
||||
line2:
|
||||
type: string
|
||||
region:
|
||||
type: string
|
||||
state:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
zipCode:
|
||||
type: string
|
||||
object.Application:
|
||||
title: Application
|
||||
type: object
|
||||
@@ -4473,6 +4700,9 @@ definitions:
|
||||
codeResendTimeout:
|
||||
type: integer
|
||||
format: int64
|
||||
cookieExpireInHours:
|
||||
type: integer
|
||||
format: int64
|
||||
createdTime:
|
||||
type: string
|
||||
defaultGroup:
|
||||
@@ -4495,6 +4725,8 @@ definitions:
|
||||
type: boolean
|
||||
enablePassword:
|
||||
type: boolean
|
||||
enableSamlAssertionSignature:
|
||||
type: boolean
|
||||
enableSamlC14n10:
|
||||
type: boolean
|
||||
enableSamlCompress:
|
||||
@@ -5136,8 +5368,6 @@ definitions:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
endTime:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
name:
|
||||
@@ -5146,23 +5376,21 @@ definitions:
|
||||
type: string
|
||||
payment:
|
||||
type: string
|
||||
planName:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
format: double
|
||||
pricingName:
|
||||
type: string
|
||||
productName:
|
||||
type: string
|
||||
productInfos:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.ProductInfo'
|
||||
products:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
startTime:
|
||||
type: string
|
||||
state:
|
||||
type: string
|
||||
updateTime:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
object.Organization:
|
||||
@@ -5173,6 +5401,8 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.AccountItem'
|
||||
accountMenu:
|
||||
type: string
|
||||
balanceCredit:
|
||||
type: number
|
||||
format: double
|
||||
@@ -5317,14 +5547,14 @@ definitions:
|
||||
type: string
|
||||
invoiceUrl:
|
||||
type: string
|
||||
isRecharge:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
order:
|
||||
type: string
|
||||
orderObj:
|
||||
$ref: '#/definitions/object.Order'
|
||||
outOrderId:
|
||||
type: string
|
||||
owner:
|
||||
@@ -5342,9 +5572,11 @@ definitions:
|
||||
price:
|
||||
type: number
|
||||
format: double
|
||||
productDisplayName:
|
||||
type: string
|
||||
productName:
|
||||
products:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
productsDisplayName:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
@@ -5352,8 +5584,6 @@ definitions:
|
||||
$ref: '#/definitions/pp.PaymentState'
|
||||
successUrl:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
user:
|
||||
@@ -5526,6 +5756,34 @@ definitions:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
object.ProductInfo:
|
||||
title: ProductInfo
|
||||
type: object
|
||||
properties:
|
||||
currency:
|
||||
type: string
|
||||
detail:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
isRecharge:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
planName:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
format: double
|
||||
pricingName:
|
||||
type: string
|
||||
quantity:
|
||||
type: integer
|
||||
format: int64
|
||||
object.PrometheusInfo:
|
||||
title: PrometheusInfo
|
||||
type: object
|
||||
@@ -5581,6 +5839,8 @@ definitions:
|
||||
type: string
|
||||
emailRegex:
|
||||
type: string
|
||||
enablePkce:
|
||||
type: boolean
|
||||
enableProxy:
|
||||
type: boolean
|
||||
enableSignAuthnRequest:
|
||||
@@ -5945,6 +6205,44 @@ definitions:
|
||||
type: boolean
|
||||
themeType:
|
||||
type: string
|
||||
object.Ticket:
|
||||
title: Ticket
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
createdTime:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.TicketMessage'
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
state:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
updatedTime:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
object.TicketMessage:
|
||||
title: TicketMessage
|
||||
type: object
|
||||
properties:
|
||||
author:
|
||||
type: string
|
||||
isAdmin:
|
||||
type: boolean
|
||||
text:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
object.Token:
|
||||
title: Token
|
||||
type: object
|
||||
@@ -6020,7 +6318,7 @@ definitions:
|
||||
application:
|
||||
type: string
|
||||
category:
|
||||
type: string
|
||||
$ref: '#/definitions/object.TransactionCategory'
|
||||
createdTime:
|
||||
type: string
|
||||
currency:
|
||||
@@ -6038,7 +6336,7 @@ definitions:
|
||||
provider:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/definitions/pp.PaymentState'
|
||||
type: string
|
||||
subtype:
|
||||
type: string
|
||||
tag:
|
||||
@@ -6047,6 +6345,13 @@ definitions:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
object.TransactionCategory:
|
||||
title: TransactionCategory
|
||||
type: string
|
||||
enum:
|
||||
- TransactionCategoryPurchase = "Purchase"
|
||||
- TransactionCategoryRecharge = "Recharge"
|
||||
example: Purchase
|
||||
object.User:
|
||||
title: User
|
||||
type: object
|
||||
@@ -6061,6 +6366,10 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
addresses:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Address'
|
||||
adfs:
|
||||
type: string
|
||||
affiliation:
|
||||
@@ -6103,6 +6412,10 @@ definitions:
|
||||
type: string
|
||||
box:
|
||||
type: string
|
||||
cart:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.ProductInfo'
|
||||
casdoor:
|
||||
type: string
|
||||
cloudfoundry:
|
||||
@@ -6312,10 +6625,10 @@ definitions:
|
||||
type: string
|
||||
onedrive:
|
||||
type: string
|
||||
originalToken:
|
||||
type: string
|
||||
originalRefreshToken:
|
||||
type: string
|
||||
originalToken:
|
||||
type: string
|
||||
oura:
|
||||
type: string
|
||||
owner:
|
||||
@@ -6486,13 +6799,13 @@ definitions:
|
||||
type: object
|
||||
properties:
|
||||
aliases:
|
||||
$ref: '#/definitions/217748.<nil>.string'
|
||||
$ref: '#/definitions/232967.<nil>.string'
|
||||
links:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.WebFingerLink'
|
||||
properties:
|
||||
$ref: '#/definitions/217806.string.string'
|
||||
$ref: '#/definitions/233025.string.string'
|
||||
subject:
|
||||
type: string
|
||||
object.WebFingerLink:
|
||||
|
||||
@@ -158,7 +158,7 @@ class AdapterEditPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} disabled={Setting.builtInObject(this.state.adapter)} style={{width: "100%"}} value={this.state.adapter.type} onChange={(value => {
|
||||
|
||||
@@ -134,7 +134,7 @@ class AdapterListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "100px",
|
||||
|
||||
@@ -870,11 +870,11 @@ class ApplicationEditPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("application:Providers"), i18next.t("general:Providers - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<ProviderTable
|
||||
title={i18next.t("general:Providers")}
|
||||
title={i18next.t("application:Providers")}
|
||||
table={this.state.application.providers}
|
||||
providers={this.state.providers}
|
||||
application={this.state.application}
|
||||
|
||||
@@ -208,7 +208,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Providers"),
|
||||
title: i18next.t("application:Providers"),
|
||||
dataIndex: "providers",
|
||||
key: "providers",
|
||||
...this.getColumnSearchProps("providers"),
|
||||
|
||||
@@ -132,7 +132,7 @@ class BaseListPage extends React.Component {
|
||||
{i18next.t("general:Search")}
|
||||
</Button>
|
||||
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
|
||||
{i18next.t("general:Reset")}
|
||||
{i18next.t("forget:Reset")}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
|
||||
@@ -22,6 +22,7 @@ import * as ProductBackend from "./backend/ProductBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import {QuantityStepper} from "./common/product/CartControls";
|
||||
|
||||
class CartListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
@@ -30,6 +31,7 @@ class CartListPage extends BaseListPage {
|
||||
...this.state,
|
||||
data: [],
|
||||
user: null,
|
||||
updatingCartItems: {},
|
||||
isPlacingOrder: false,
|
||||
loading: false,
|
||||
pagination: {
|
||||
@@ -40,6 +42,8 @@ class CartListPage extends BaseListPage {
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
};
|
||||
|
||||
this.updatingCartItemsRef = {};
|
||||
}
|
||||
|
||||
clearCart() {
|
||||
@@ -90,6 +94,9 @@ class CartListPage extends BaseListPage {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const order = res.data;
|
||||
const user = Setting.deepCopy(this.state.user);
|
||||
user.cart = [];
|
||||
UserBackend.updateUser(user.owner, user.name, user);
|
||||
Setting.showMessage("success", i18next.t("product:Order created successfully"));
|
||||
Setting.goToLink(`/orders/${order.owner}/${order.name}/pay`);
|
||||
} else {
|
||||
@@ -132,6 +139,66 @@ class CartListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
updateCartItemQuantity(record, newQuantity) {
|
||||
if (newQuantity < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemKey = `${record.name}-${record.price}-${record.pricingName || ""}-${record.planName || ""}`;
|
||||
if (this.updatingCartItemsRef?.[itemKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatingCartItemsRef[itemKey] = true;
|
||||
|
||||
const user = Setting.deepCopy(this.state.user);
|
||||
const index = user.cart.findIndex(item => item.name === record.name && item.price === record.price && (item.pricingName || "") === (record.pricingName || "") && (item.planName || "") === (record.planName || ""));
|
||||
if (index === -1) {
|
||||
delete this.updatingCartItemsRef[itemKey];
|
||||
return;
|
||||
}
|
||||
|
||||
if (index !== -1) {
|
||||
user.cart[index].quantity = newQuantity;
|
||||
|
||||
const newData = [...this.state.data];
|
||||
const dataIndex = newData.findIndex(item => item.name === record.name && item.price === record.price && (item.pricingName || "") === (record.pricingName || "") && (item.planName || "") === (record.planName || ""));
|
||||
if (dataIndex !== -1) {
|
||||
newData[dataIndex].quantity = newQuantity;
|
||||
this.setState({data: newData});
|
||||
}
|
||||
|
||||
this.setState(prevState => ({
|
||||
updatingCartItems: {
|
||||
...(prevState.updatingCartItems || {}),
|
||||
[itemKey]: true,
|
||||
},
|
||||
}));
|
||||
|
||||
UserBackend.updateUser(user.owner, user.name, user)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({user: user});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.fetch();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
this.fetch();
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.updatingCartItemsRef[itemKey];
|
||||
this.setState(prevState => {
|
||||
const updatingCartItems = {...(prevState.updatingCartItems || {})};
|
||||
delete updatingCartItems[itemKey];
|
||||
return {updatingCartItems};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderTable(carts) {
|
||||
const isEmpty = carts === undefined || carts === null || carts.length === 0;
|
||||
const owner = this.state.user?.owner || this.props.account.owner;
|
||||
@@ -183,7 +250,7 @@ class CartListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("product:Price"),
|
||||
title: i18next.t("order:Price"),
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
width: "160px",
|
||||
@@ -227,8 +294,22 @@ class CartListPage extends BaseListPage {
|
||||
title: i18next.t("product:Quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
width: "120px",
|
||||
width: "100px",
|
||||
sorter: true,
|
||||
render: (text, record) => {
|
||||
const itemKey = `${record.name}-${record.price}-${record.pricingName || ""}-${record.planName || ""}`;
|
||||
const isUpdating = this.state.updatingCartItems?.[itemKey] === true;
|
||||
return (
|
||||
<QuantityStepper
|
||||
value={text}
|
||||
min={1}
|
||||
onIncrease={() => this.updateCartItemQuantity(record, text + 1)}
|
||||
onDecrease={() => this.updateCartItemQuantity(record, text - 1)}
|
||||
onChange={null}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
@@ -240,7 +321,7 @@ class CartListPage extends BaseListPage {
|
||||
return (
|
||||
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
|
||||
<Button type="primary" onClick={() => this.props.history.push(`/products/${owner}/${record.name}/buy`)}>
|
||||
{i18next.t("product:Detail")}
|
||||
{i18next.t("general:Detail")}
|
||||
</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
@@ -338,13 +419,28 @@ class CartListPage extends BaseListPage {
|
||||
|
||||
const fullCartData = await Promise.all(productPromises);
|
||||
|
||||
const sortedData = [...fullCartData];
|
||||
if (params.sortField && params.sortOrder) {
|
||||
sortedData.sort((a, b) => {
|
||||
const aValue = a[params.sortField];
|
||||
const bValue = b[params.sortField];
|
||||
|
||||
if (aValue === bValue) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const comparison = aValue > bValue ? 1 : -1;
|
||||
return params.sortOrder === "ascend" ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: fullCartData,
|
||||
data: sortedData,
|
||||
user: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: fullCartData.length,
|
||||
total: sortedData.length,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
|
||||
@@ -149,7 +149,7 @@ class CertEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.type} onChange={(value => {
|
||||
|
||||
@@ -147,7 +147,7 @@ class CertListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
filterMultiple: false,
|
||||
|
||||
@@ -93,7 +93,7 @@ class FormEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select
|
||||
@@ -115,7 +115,7 @@ class FormEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Input value={this.state.form.tag} onChange={e => {
|
||||
|
||||
@@ -148,7 +148,7 @@ class GroupEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select style={{width: "100%"}}
|
||||
|
||||
@@ -92,7 +92,7 @@ class GroupListPage extends BaseListPage {
|
||||
uploadFile(info) {
|
||||
const {status, msg} = info;
|
||||
if (status === "ok") {
|
||||
Setting.showMessage("success", "Groups uploaded successfully, refreshing the page");
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
const {pagination} = this.state;
|
||||
this.fetch({pagination});
|
||||
} else if (status === "error") {
|
||||
|
||||
@@ -218,7 +218,7 @@ class LdapEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Input value={this.state.ldap.username} onChange={e => {
|
||||
|
||||
@@ -188,7 +188,7 @@ function ManagementPage(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown key="/rightDropDown" menu={{items, onClick}} >
|
||||
<Dropdown key="/rightDropDown" menu={{items, onClick}} placement="bottomRight" >
|
||||
<div className="rightDropDown">
|
||||
{
|
||||
renderAvatar()
|
||||
@@ -320,7 +320,7 @@ function ManagementPage(props) {
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/applications">{i18next.t("general:Identity")}</Link>, "/identity", <LockTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/applications">{i18next.t("general:Applications")}</Link>, "/applications"),
|
||||
Setting.getItem(<Link to="/providers">{i18next.t("general:Providers")}</Link>, "/providers"),
|
||||
Setting.getItem(<Link to="/providers">{i18next.t("application:Providers")}</Link>, "/providers"),
|
||||
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
]));
|
||||
|
||||
@@ -158,7 +158,7 @@ class OrderEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Products")}:
|
||||
{i18next.t("general:Products")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select
|
||||
@@ -195,7 +195,7 @@ class OrderEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Payment")}:
|
||||
{i18next.t("general:Payment")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.payment} disabled={isViewMode} onChange={(value) => {
|
||||
@@ -231,7 +231,7 @@ class OrderEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Message")}:
|
||||
{i18next.t("payment:Message")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.message} onChange={e => {
|
||||
@@ -239,26 +239,6 @@ class OrderEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Start time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.startTime} onChange={e => {
|
||||
this.updateOrderField("startTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:End time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.endTime} onChange={e => {
|
||||
this.updateOrderField("endTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, List, Table, Tooltip} from "antd";
|
||||
import {Button, Col, List, Row, Table, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
@@ -37,8 +37,6 @@ class OrderListPage extends BaseListPage {
|
||||
payment: "",
|
||||
state: "Created",
|
||||
message: "",
|
||||
startTime: moment().format(),
|
||||
endTime: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,7 +137,7 @@ class OrderListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:Products"),
|
||||
title: i18next.t("general:Products"),
|
||||
dataIndex: "products",
|
||||
key: "products",
|
||||
...this.getColumnSearchProps("products"),
|
||||
@@ -159,21 +157,31 @@ class OrderListPage extends BaseListPage {
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
renderItem={(productInfo, i) => {
|
||||
const price = productInfo.price * (productInfo.quantity || 1);
|
||||
const price = productInfo.price || 0;
|
||||
const number = productInfo.quantity || 1;
|
||||
const currency = record.currency || "USD";
|
||||
const productName = productInfo.displayName || productInfo.name;
|
||||
return (
|
||||
<List.Item>
|
||||
<div style={{display: "inline"}}>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
|
||||
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
|
||||
</Tooltip>
|
||||
<Link to={`/products/${record.owner}/${productInfo.name}`}>
|
||||
{productInfo.displayName || productInfo.name}
|
||||
</Link>
|
||||
<span style={{marginLeft: "8px", color: "#666"}}>
|
||||
{Setting.getPriceDisplay(price, currency)}
|
||||
</span>
|
||||
</div>
|
||||
<Row style={{width: "100%"}} wrap={false} gutter={[12, 0]}>
|
||||
<Col flex="auto" style={{minWidth: 0}}>
|
||||
<div style={{display: "flex", alignItems: "center", minWidth: 0}}>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
|
||||
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={productName}>
|
||||
<Link to={`/products/${record.owner}/${productInfo.name}`} style={{display: "inline-block", maxWidth: "100%", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>
|
||||
{productName}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Col>
|
||||
<Col flex="none" style={{whiteSpace: "nowrap"}}>
|
||||
<span style={{color: "#666"}}>
|
||||
{Setting.getCurrencySymbol(currency)}{price} ({Setting.getCurrencyText(currency)}) × {number}
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
@@ -229,29 +237,6 @@ class OrderListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("state"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Start time"),
|
||||
dataIndex: "startTime",
|
||||
key: "startTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:End time"),
|
||||
dataIndex: "endTime",
|
||||
key: "endTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
if (text === "") {
|
||||
return "(empty)";
|
||||
}
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
|
||||
@@ -233,7 +233,7 @@ class OrderPayPage extends React.Component {
|
||||
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{objectFit: "contain"}} />
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={1}>
|
||||
<Descriptions.Item label={i18next.t("order:Price")} span={1}>
|
||||
<span style={{fontSize: 18, fontWeight: "bold"}}>
|
||||
{this.getProductPrice(product)}
|
||||
</span>
|
||||
@@ -245,7 +245,7 @@ class OrderPayPage extends React.Component {
|
||||
</Descriptions.Item>
|
||||
|
||||
{product?.detail && (
|
||||
<Descriptions.Item label={i18next.t("product:Detail")} span={2}>
|
||||
<Descriptions.Item label={i18next.t("general:Detail")} span={2}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
@@ -267,6 +267,17 @@ class OrderPayPage extends React.Component {
|
||||
render() {
|
||||
const {order, productInfos} = this.state;
|
||||
|
||||
const updateTime = order?.updateTime || "";
|
||||
const state = order?.state || "";
|
||||
const updateTimeMap = {
|
||||
Paid: i18next.t("order:Payment time"),
|
||||
Canceled: i18next.t("order:Cancel time"),
|
||||
PaymentFailed: i18next.t("order:Payment failed time"),
|
||||
Timeout: i18next.t("order:Timeout time"),
|
||||
};
|
||||
const updateTimeLabel = updateTimeMap[state] || i18next.t("general:Updated time");
|
||||
const shouldShowUpdateTime = state !== "Created" && updateTime !== "";
|
||||
|
||||
if (!order || !productInfos) {
|
||||
return null;
|
||||
}
|
||||
@@ -275,7 +286,7 @@ class OrderPayPage extends React.Component {
|
||||
<div className="login-content">
|
||||
<Spin spinning={this.state.isProcessingPayment} size="large" tip={i18next.t("product:Processing payment...")} style={{paddingTop: "10%"}} >
|
||||
<div style={{marginBottom: "20px"}}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("general:Order")}</span>} bordered column={3}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("application:Order")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("general:ID")} span={3}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{order.name}
|
||||
@@ -291,6 +302,13 @@ class OrderPayPage extends React.Component {
|
||||
{Setting.getFormattedDate(order.createdTime)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
{shouldShowUpdateTime && (
|
||||
<Descriptions.Item label={updateTimeLabel}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{Setting.getFormattedDate(updateTime)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label={i18next.t("general:User")}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{order.user}
|
||||
@@ -307,14 +325,14 @@ class OrderPayPage extends React.Component {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("order:Payment")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("general:Payment")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("order:Price")} span={3}>
|
||||
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
|
||||
{this.getPrice(order)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
{!this.state.isViewMode && (
|
||||
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
|
||||
<Descriptions.Item label={i18next.t("order:Pay")} span={3}>
|
||||
{this.renderPaymentMethods()}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
|
||||
@@ -407,7 +407,7 @@ class OrganizationEditPage extends React.Component {
|
||||
}}
|
||||
filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
>
|
||||
{Setting.getCountryCodeOption({name: i18next.t("organization:All"), code: "All", phone: 0})}
|
||||
{Setting.getCountryCodeOption({name: i18next.t("general:All"), code: "All", phone: 0})}
|
||||
{
|
||||
Setting.getCountryCodeData().map((country) => Setting.getCountryCodeOption(country))
|
||||
}
|
||||
@@ -481,7 +481,7 @@ class OrganizationEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("application:Tags - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.organization.tags} onChange={(value => {this.updateOrganizationField("tags", value);})}>
|
||||
@@ -678,6 +678,16 @@ class OrganizationEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Account menu"), i18next.t("organization:Account menu - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.accountMenu || "Horizontal"} onChange={(value => {this.updateOrganizationField("accountMenu", value);})}
|
||||
options={[{value: "Horizontal", label: i18next.t("application:Horizontal")}, {value: "Vertical", label: i18next.t("application:Vertical")}].map(item => Setting.getOption(item.label, item.value))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
||||
@@ -817,7 +827,7 @@ class OrganizationEditPage extends React.Component {
|
||||
this.state.organization !== null ? this.renderOrganization() : null
|
||||
}
|
||||
{this.state.mode !== "add" && this.state.transactions.length > 0 ? (
|
||||
<Card size="small" title={i18next.t("transaction:Transactions")} style={{marginTop: "20px"}} type="inner">
|
||||
<Card size="small" title={i18next.t("general:Transactions")} style={{marginTop: "20px"}} type="inner">
|
||||
<TransactionTable transactions={this.state.transactions} includeUser={true} />
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
@@ -232,7 +232,7 @@ class PaymentEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("payment:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.payment.type} onChange={e => {
|
||||
@@ -242,7 +242,7 @@ class PaymentEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Products"), i18next.t("payment:Products - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Products"), i18next.t("payment:Products - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select
|
||||
@@ -265,7 +265,7 @@ class PaymentEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.payment.price} onChange={e => {
|
||||
@@ -456,7 +456,7 @@ class PaymentEditPage extends React.Component {
|
||||
}
|
||||
|
||||
if (!Setting.isValidEmail(this.state.payment.personEmail)) {
|
||||
return i18next.t("signup:The input is not valid Email!");
|
||||
return i18next.t("login:The input is not valid Email!");
|
||||
}
|
||||
|
||||
if (!Setting.isValidPhone(this.state.payment.personPhone)) {
|
||||
|
||||
@@ -161,7 +161,7 @@ class PaymentListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "140px",
|
||||
@@ -175,7 +175,7 @@ class PaymentListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:Products"),
|
||||
title: i18next.t("general:Products"),
|
||||
dataIndex: "products",
|
||||
key: "products",
|
||||
...this.getColumnSearchProps("products"),
|
||||
@@ -219,7 +219,7 @@ class PaymentListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("product:Price"),
|
||||
title: i18next.t("order:Price"),
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
width: "160px",
|
||||
|
||||
@@ -271,7 +271,7 @@ class PermissionEditPage extends React.Component {
|
||||
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.users}
|
||||
onChange={(value => {this.updatePermissionField("users", value);})}
|
||||
options={[
|
||||
Setting.getOption(i18next.t("organization:All"), "*"),
|
||||
Setting.getOption(i18next.t("general:All"), "*"),
|
||||
...this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`)),
|
||||
]}
|
||||
/>
|
||||
@@ -285,7 +285,7 @@ class PermissionEditPage extends React.Component {
|
||||
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.groups}
|
||||
onChange={(value => {this.updatePermissionField("groups", value);})}
|
||||
options={[
|
||||
Setting.getOption(i18next.t("organization:All"), "*"),
|
||||
Setting.getOption(i18next.t("general:All"), "*"),
|
||||
...this.state.groups.map((group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`)),
|
||||
]}
|
||||
/>
|
||||
@@ -299,7 +299,7 @@ class PermissionEditPage extends React.Component {
|
||||
<Select disabled={!this.hasRoleDefinition(this.state.model)} placeholder={this.hasRoleDefinition(this.state.model) ? "" : "This field is disabled because the model is empty or it doesn't support RBAC (in another word, doesn't contain [role_definition])"} virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.roles}
|
||||
onChange={(value => {this.updatePermissionField("roles", value);})}
|
||||
options={[
|
||||
Setting.getOption(i18next.t("organization:All"), "*"),
|
||||
Setting.getOption(i18next.t("general:All"), "*"),
|
||||
...this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission) => Setting.getOption(`${permission.owner}/${permission.name}`, `${permission.owner}/${permission.name}`)),
|
||||
]}
|
||||
/>
|
||||
@@ -315,7 +315,7 @@ class PermissionEditPage extends React.Component {
|
||||
this.updatePermissionField("domains", value);
|
||||
})}
|
||||
options={[
|
||||
Setting.getOption(i18next.t("organization:All"), "*"),
|
||||
Setting.getOption(i18next.t("general:All"), "*"),
|
||||
...this.state.permission.domains.filter(domain => domain !== "*").map((domain) => Setting.getOption(domain, domain)),
|
||||
]}
|
||||
/>
|
||||
@@ -349,7 +349,7 @@ class PermissionEditPage extends React.Component {
|
||||
options={this.state.permission.resourceType === "API" ? Setting.getApiPaths().map((option, index) => {
|
||||
return Setting.getOption(option, option);
|
||||
}) : [
|
||||
Setting.getOption(i18next.t("organization:All"), "*"),
|
||||
Setting.getOption(i18next.t("general:All"), "*"),
|
||||
...this.state.resources.map((resource) => Setting.getOption(`${resource.name}`, `${resource.name}`)),
|
||||
]}
|
||||
/>
|
||||
@@ -369,7 +369,7 @@ class PermissionEditPage extends React.Component {
|
||||
] : [
|
||||
{value: "Read", name: i18next.t("permission:Read")},
|
||||
{value: "Write", name: i18next.t("permission:Write")},
|
||||
{value: "Admin", name: i18next.t("permission:Admin")},
|
||||
{value: "Admin", name: i18next.t("general:Admin")},
|
||||
].map((item) => Setting.getOption(item.name, item.value))}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -337,7 +337,7 @@ class PermissionListPage extends BaseListPage {
|
||||
case "Write":
|
||||
return i18next.t("permission:Write");
|
||||
case "Admin":
|
||||
return i18next.t("permission:Admin");
|
||||
return i18next.t("general:Admin");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ class PlanEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("plan:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.plan.price} disabled={isViewMode} onChange={value => {
|
||||
@@ -260,6 +260,16 @@ class PlanEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("plan:Is exclusive"), i18next.t("plan:Is exclusive - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.plan.isExclusive} disabled={isViewMode} onChange={checked => {
|
||||
this.updatePlanField("isExclusive", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class PlanListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price"),
|
||||
title: i18next.t("order:Price"),
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
width: "160px",
|
||||
@@ -154,7 +154,7 @@ class PlanListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("role"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/roles/${record.owner}/${text}`}>
|
||||
<Link to={`/roles/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as PricingBackend from "./backend/PricingBackend";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import {FloatingCartButton, QuantityStepper} from "./common/product/CartControls";
|
||||
|
||||
class ProductBuyPage extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -40,6 +41,8 @@ class ProductBuyPage extends React.Component {
|
||||
isPlacingOrder: false,
|
||||
isAddingToCart: false,
|
||||
customPrice: 100,
|
||||
buyQuantity: params.get("quantity") ? parseInt(params.get("quantity"), 10) : 1,
|
||||
cartItemCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,6 +61,22 @@ class ProductBuyPage extends React.Component {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getProduct();
|
||||
this.getPaymentEnv();
|
||||
this.getCartItemCount();
|
||||
}
|
||||
|
||||
getCartItemCount() {
|
||||
if (!this.props.account) {
|
||||
return;
|
||||
}
|
||||
const userOwner = this.props.account.owner;
|
||||
const userName = this.props.account.name;
|
||||
UserBackend.getUser(userOwner, userName).then((res) => {
|
||||
if (res.status === "ok" && res.data.cart) {
|
||||
this.setState({
|
||||
cartItemCount: res.data.cart.length,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setStateAsync(state) {
|
||||
@@ -175,9 +194,10 @@ class ProductBuyPage extends React.Component {
|
||||
}
|
||||
|
||||
const existingItemIndex = cart.findIndex(item => item.name === product.name && item.price === actualPrice && (item.pricingName || "") === pricingName && (item.planName || "") === planName);
|
||||
const quantityToAdd = this.state.buyQuantity;
|
||||
|
||||
if (existingItemIndex !== -1) {
|
||||
cart[existingItemIndex].quantity += 1;
|
||||
cart[existingItemIndex].quantity = (cart[existingItemIndex].quantity ?? 1) + quantityToAdd;
|
||||
} else {
|
||||
const newProductInfo = {
|
||||
name: product.name,
|
||||
@@ -185,7 +205,7 @@ class ProductBuyPage extends React.Component {
|
||||
currency: product.currency,
|
||||
pricingName: pricingName,
|
||||
planName: planName,
|
||||
quantity: 1,
|
||||
quantity: quantityToAdd,
|
||||
};
|
||||
cart.push(newProductInfo);
|
||||
}
|
||||
@@ -195,6 +215,9 @@ class ProductBuyPage extends React.Component {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
this.setState({
|
||||
cartItemCount: cart.length,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
@@ -230,7 +253,7 @@ class ProductBuyPage extends React.Component {
|
||||
price: product.isRecharge ? customPrice : product.price,
|
||||
pricingName: pricingName,
|
||||
planName: planName,
|
||||
quantity: 1,
|
||||
quantity: this.state.buyQuantity,
|
||||
}];
|
||||
|
||||
OrderBackend.placeOrder(product.owner, productInfos, this.state.userName ?? "")
|
||||
@@ -322,23 +345,20 @@ class ProductBuyPage extends React.Component {
|
||||
const isAmountZero = product.isRecharge && (this.state.customPrice === 0 || this.state.customPrice === null);
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center", gap: "20px"}}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center", gap: "25px"}}>
|
||||
<QuantityStepper
|
||||
value={this.state.buyQuantity}
|
||||
min={1}
|
||||
onIncrease={() => this.setState(prevState => ({buyQuantity: prevState.buyQuantity + 1}))}
|
||||
onDecrease={() => this.setState(prevState => ({buyQuantity: Math.max(1, prevState.buyQuantity - 1)}))}
|
||||
onChange={(val) => this.setState({buyQuantity: val || 1})}
|
||||
disabled={isRechargeUnpurchasable || this.state.isAddingToCart || isAmountZero}
|
||||
style={{
|
||||
height: "50px",
|
||||
fontSize: "18px",
|
||||
borderRadius: "30px",
|
||||
paddingLeft: "60px",
|
||||
paddingRight: "60px",
|
||||
width: "140px",
|
||||
}}
|
||||
onClick={() => this.placeOrder(product)}
|
||||
disabled={this.state.isPlacingOrder || isRechargeUnpurchasable || isAmountZero}
|
||||
loading={this.state.isPlacingOrder}
|
||||
>
|
||||
{i18next.t("order:Place Order")}
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
@@ -346,8 +366,8 @@ class ProductBuyPage extends React.Component {
|
||||
height: "50px",
|
||||
fontSize: "18px",
|
||||
borderRadius: "30px",
|
||||
paddingLeft: "30px",
|
||||
paddingRight: "30px",
|
||||
paddingLeft: "40px",
|
||||
paddingRight: "40px",
|
||||
}}
|
||||
onClick={() => this.addToCart(product)}
|
||||
disabled={isRechargeUnpurchasable || this.state.isAddingToCart || isAmountZero}
|
||||
@@ -355,6 +375,22 @@ class ProductBuyPage extends React.Component {
|
||||
>
|
||||
{i18next.t("product:Add to cart")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
style={{
|
||||
height: "50px",
|
||||
fontSize: "18px",
|
||||
borderRadius: "30px",
|
||||
paddingLeft: "40px",
|
||||
paddingRight: "40px",
|
||||
}}
|
||||
onClick={() => this.placeOrder(product)}
|
||||
disabled={this.state.isPlacingOrder || isRechargeUnpurchasable || isAmountZero}
|
||||
loading={this.state.isPlacingOrder}
|
||||
>
|
||||
{i18next.t("general:Place Order")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -369,6 +405,10 @@ class ProductBuyPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="login-content">
|
||||
<FloatingCartButton
|
||||
itemCount={this.state.cartItemCount}
|
||||
onClick={() => this.props.history.push("/cart")}
|
||||
/>
|
||||
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 20} : {fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
|
||||
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
||||
@@ -376,7 +416,7 @@ class ProductBuyPage extends React.Component {
|
||||
{Setting.getLanguageText(product?.displayName)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Detail")}><span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span></Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:Detail")}><span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span></Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("user:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
|
||||
@@ -384,12 +424,12 @@ class ProductBuyPage extends React.Component {
|
||||
</Descriptions.Item>
|
||||
{
|
||||
product.isRecharge ? (
|
||||
<Descriptions.Item span={3} label={i18next.t("product:Price")}>
|
||||
<Descriptions.Item span={3} label={i18next.t("order:Price")}>
|
||||
{this.renderRechargeInput(product)}
|
||||
</Descriptions.Item>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Descriptions.Item label={i18next.t("product:Price")}>
|
||||
<Descriptions.Item label={i18next.t("order:Price")}>
|
||||
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
|
||||
{
|
||||
this.getPrice(product)
|
||||
@@ -401,7 +441,7 @@ class ProductBuyPage extends React.Component {
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Descriptions.Item label={i18next.t("order:Place Order")} span={3}>
|
||||
<Descriptions.Item label={i18next.t("general:Place Order")} span={3}>
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center", minHeight: "80px"}}>
|
||||
{placeOrderButton}
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,7 @@ class ProductEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("product:Detail"), i18next.t("product:Detail - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Detail"), i18next.t("product:Detail - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.product.detail} disabled={isViewMode} onChange={e => {
|
||||
@@ -266,7 +266,7 @@ class ProductEditPage extends React.Component {
|
||||
) : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.product.price} disabled={isViewMode || isCreatedByPlan} onChange={value => {
|
||||
|
||||
@@ -153,7 +153,7 @@ class ProductListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("tag"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("product:Price"),
|
||||
title: i18next.t("order:Price"),
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
width: "160px",
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as Setting from "./Setting";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import i18next from "i18next";
|
||||
import {FloatingCartButton, QuantityStepper} from "./common/product/CartControls";
|
||||
|
||||
const {Text, Title} = Typography;
|
||||
|
||||
@@ -30,14 +31,57 @@ class ProductStorePage extends React.Component {
|
||||
products: [],
|
||||
loading: true,
|
||||
addingToCartProducts: [],
|
||||
productQuantities: {},
|
||||
cartItemCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.account) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getProducts();
|
||||
this.getCartItemCount();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.account && this.props.account) {
|
||||
this.getProducts();
|
||||
this.getCartItemCount();
|
||||
}
|
||||
}
|
||||
|
||||
getCartItemCount() {
|
||||
if (!this.props.account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userOwner = this.props.account.owner;
|
||||
const userName = this.props.account.name;
|
||||
UserBackend.getUser(userOwner, userName).then((res) => {
|
||||
if (res.status === "ok" && res.data.cart) {
|
||||
this.setState({
|
||||
cartItemCount: res.data.cart.length,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateProductQuantity(productName, value) {
|
||||
this.setState(prevState => ({
|
||||
productQuantities: {
|
||||
...prevState.productQuantities,
|
||||
[productName]: value,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
if (!this.props.account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageSize = 100; // Max products to display in the store
|
||||
const owner = Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account);
|
||||
this.setState({loading: true});
|
||||
@@ -85,9 +129,10 @@ class ProductStorePage extends React.Component {
|
||||
}
|
||||
|
||||
const existingItemIndex = cart.findIndex(item => item.name === product.name && item.price === product.price);
|
||||
const quantityToAdd = this.state.productQuantities[product.name] || 1;
|
||||
|
||||
if (existingItemIndex !== -1) {
|
||||
cart[existingItemIndex].quantity += 1;
|
||||
cart[existingItemIndex].quantity = (cart[existingItemIndex].quantity ?? 1) + quantityToAdd;
|
||||
} else {
|
||||
const newCartProductInfo = {
|
||||
name: product.name,
|
||||
@@ -95,7 +140,7 @@ class ProductStorePage extends React.Component {
|
||||
currency: product.currency,
|
||||
pricingName: "",
|
||||
planName: "",
|
||||
quantity: 1,
|
||||
quantity: quantityToAdd,
|
||||
};
|
||||
cart.push(newCartProductInfo);
|
||||
}
|
||||
@@ -105,6 +150,9 @@ class ProductStorePage extends React.Component {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
this.setState({
|
||||
cartItemCount: cart.length,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
@@ -127,11 +175,14 @@ class ProductStorePage extends React.Component {
|
||||
}
|
||||
|
||||
handleBuyProduct(product) {
|
||||
this.props.history.push(`/products/${product.owner}/${product.name}/buy`);
|
||||
const quantity = this.state.productQuantities[product.name] || 1;
|
||||
this.props.history.push(`/products/${product.owner}/${product.name}/buy?quantity=${quantity}`);
|
||||
}
|
||||
|
||||
renderProductCard(product) {
|
||||
const isAdding = this.state.addingToCartProducts.includes(product.name);
|
||||
const quantity = this.state.productQuantities[product.name] || 1;
|
||||
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={`${product.owner}/${product.name}`} style={{marginBottom: "20px"}}>
|
||||
<Card
|
||||
@@ -149,6 +200,40 @@ class ProductStorePage extends React.Component {
|
||||
}
|
||||
actions={[
|
||||
<div key="actions" style={{display: "flex", justifyContent: "center", gap: "10px", width: "100%", padding: "0 10px"}} onClick={(e) => e.stopPropagation()}>
|
||||
{!product.isRecharge && (
|
||||
<>
|
||||
<QuantityStepper
|
||||
value={quantity}
|
||||
min={1}
|
||||
onIncrease={() => this.updateProductQuantity(product.name, quantity + 1)}
|
||||
onDecrease={() => this.updateProductQuantity(product.name, Math.max(1, quantity - 1))}
|
||||
onChange={(val) => this.updateProductQuantity(product.name, val || 1)}
|
||||
disabled={isAdding}
|
||||
style={{
|
||||
height: "45px",
|
||||
fontSize: "16px",
|
||||
width: "120px",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
key="add"
|
||||
type="default"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.addToCart(product);
|
||||
}}
|
||||
style={{
|
||||
width: "150px",
|
||||
height: "45px",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
disabled={isAdding}
|
||||
loading={isAdding}
|
||||
>
|
||||
{i18next.t("product:Add to cart")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
key="buy"
|
||||
type="primary"
|
||||
@@ -156,23 +241,14 @@ class ProductStorePage extends React.Component {
|
||||
e.stopPropagation();
|
||||
this.handleBuyProduct(product);
|
||||
}}
|
||||
style={{
|
||||
width: "150px",
|
||||
height: "45px",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
{i18next.t("product:Buy")}
|
||||
</Button>
|
||||
{!product.isRecharge && (
|
||||
<Button
|
||||
key="add"
|
||||
type="default"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.addToCart(product);
|
||||
}}
|
||||
disabled={isAdding}
|
||||
loading={isAdding}
|
||||
>
|
||||
{i18next.t("product:Add to cart")}
|
||||
</Button>
|
||||
)}
|
||||
</div>,
|
||||
]}
|
||||
bodyStyle={{flex: 1, display: "flex", flexDirection: "column"}}
|
||||
@@ -253,6 +329,10 @@ class ProductStorePage extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<FloatingCartButton
|
||||
itemCount={this.state.cartItemCount}
|
||||
onClick={() => this.props.history.push("/cart")}
|
||||
/>
|
||||
<Row gutter={[16, 16]}>
|
||||
{this.state.loading ? (
|
||||
<Col span={24}>
|
||||
|
||||
@@ -257,7 +257,7 @@ class ProviderEditPage extends React.Component {
|
||||
<Input value={this.state.provider.userMapping.affiliation} onChange={e => {
|
||||
this.updateUserMappingField("affiliation", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Title"), i18next.t("general:Title - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.title} onChange={e => {
|
||||
this.updateUserMappingField("title", e.target.value);
|
||||
}} />
|
||||
@@ -319,7 +319,7 @@ class ProviderEditPage extends React.Component {
|
||||
return Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"));
|
||||
case "SMS":
|
||||
if (provider.type === "Volc Engine SMS" || provider.type === "Amazon SNS" || provider.type === "Baidu Cloud SMS") {
|
||||
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
|
||||
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
|
||||
} else if (provider.type === "Huawei Cloud SMS") {
|
||||
return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"));
|
||||
} else if (provider.type === "UCloud SMS") {
|
||||
@@ -331,19 +331,19 @@ class ProviderEditPage extends React.Component {
|
||||
}
|
||||
case "Captcha":
|
||||
if (provider.type === "Aliyun Captcha") {
|
||||
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
|
||||
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
|
||||
} else {
|
||||
return Setting.getLabel(i18next.t("provider:Site key"), i18next.t("provider:Site key - Tooltip"));
|
||||
}
|
||||
case "Notification":
|
||||
if (provider.type === "DingTalk") {
|
||||
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
|
||||
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
|
||||
} else {
|
||||
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
|
||||
}
|
||||
case "ID Verification":
|
||||
if (provider.type === "Alibaba Cloud") {
|
||||
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
|
||||
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
|
||||
} else {
|
||||
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
|
||||
}
|
||||
@@ -751,7 +751,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} showSearch value={this.state.provider.type} onChange={(value => {
|
||||
@@ -816,7 +816,7 @@ class ProviderEditPage extends React.Component {
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "Normal", name: i18next.t("provider:Normal")},
|
||||
{id: "Normal", name: i18next.t("application:Normal")},
|
||||
{id: "Silent", name: i18next.t("provider:Silent")},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
@@ -880,7 +880,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.scopes} onChange={e => {
|
||||
@@ -1127,7 +1127,7 @@ class ProviderEditPage extends React.Component {
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{["Casdoor"].includes(this.state.provider.type) ?
|
||||
Setting.getLabel(i18next.t("general:Provider"), i18next.t("provider:Provider - Tooltip"))
|
||||
Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))
|
||||
: Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
@@ -1173,7 +1173,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
) : null}
|
||||
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO", "CUCloud"].includes(this.state.provider.type) ? (
|
||||
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO", "CUCloud", "Alibaba Cloud PNVS SMS"].includes(this.state.provider.type) ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{["Casdoor"].includes(this.state.provider.type) ?
|
||||
@@ -1758,7 +1758,7 @@ class ProviderEditPage extends React.Component {
|
||||
copy(`${authConfig.serverUrl}/api/acs`);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}}>
|
||||
{i18next.t("provider:Copy")}
|
||||
{i18next.t("general:Copy")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -1774,7 +1774,7 @@ class ProviderEditPage extends React.Component {
|
||||
copy(`${authConfig.serverUrl}/api/acs`);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}}>
|
||||
{i18next.t("provider:Copy")}
|
||||
{i18next.t("general:Copy")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -158,7 +158,7 @@ class ProviderListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "110px",
|
||||
@@ -243,7 +243,7 @@ class ProviderListPage extends BaseListPage {
|
||||
<Table scroll={{x: "max-content"}} columns={filteredColumns} dataSource={providers} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Providers")}
|
||||
{i18next.t("application:Providers")}
|
||||
<Button id="add-button" type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -189,7 +189,7 @@ class ResourceListPage extends BaseListPage {
|
||||
// sorter: (a, b) => a.fileName.localeCompare(b.fileName),
|
||||
// },
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "fileType",
|
||||
key: "fileType",
|
||||
width: "80px",
|
||||
|
||||
@@ -498,11 +498,11 @@ export const GetTranslatedUserItems = () => {
|
||||
{name: "Location", label: i18next.t("user:Location")},
|
||||
{name: "Address", label: i18next.t("user:Address")},
|
||||
{name: "Affiliation", label: i18next.t("user:Affiliation")},
|
||||
{name: "Title", label: i18next.t("user:Title")},
|
||||
{name: "Title", label: i18next.t("general:Title")},
|
||||
{name: "ID card type", label: i18next.t("user:ID card type")},
|
||||
{name: "ID card", label: i18next.t("user:ID card")},
|
||||
{name: "ID card info", label: i18next.t("user:ID card info")},
|
||||
{name: "Real name", label: i18next.t("user:Real name")},
|
||||
{name: "Real name", label: i18next.t("application:Real name")},
|
||||
{name: "ID verification", label: i18next.t("user:ID verification")},
|
||||
{name: "Homepage", label: i18next.t("user:Homepage")},
|
||||
{name: "Bio", label: i18next.t("user:Bio")},
|
||||
@@ -514,7 +514,7 @@ export const GetTranslatedUserItems = () => {
|
||||
{name: "Balance", label: i18next.t("user:Balance")},
|
||||
{name: "Balance currency", label: i18next.t("organization:Balance currency")},
|
||||
{name: "Balance credit", label: i18next.t("organization:Balance credit")},
|
||||
{name: "Transactions", label: i18next.t("transaction:Transactions")},
|
||||
{name: "Transactions", label: i18next.t("general:Transactions")},
|
||||
{name: "Score", label: i18next.t("user:Score")},
|
||||
{name: "Karma", label: i18next.t("user:Karma")},
|
||||
{name: "Ranking", label: i18next.t("user:Ranking")},
|
||||
@@ -531,10 +531,10 @@ export const GetTranslatedUserItems = () => {
|
||||
{name: "Is deleted", label: i18next.t("user:Is deleted")},
|
||||
{name: "Need update password", label: i18next.t("user:Need update password")},
|
||||
{name: "IP whitelist", label: i18next.t("general:IP whitelist")},
|
||||
{name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")},
|
||||
{name: "Multi-factor authentication", label: i18next.t("mfa:Multi-factor authentication")},
|
||||
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
|
||||
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
||||
{name: "Face ID", label: i18next.t("user:Face ID")},
|
||||
{name: "Face ID", label: i18next.t("login:Face ID")},
|
||||
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
|
||||
{name: "MFA items", label: i18next.t("general:MFA items")},
|
||||
];
|
||||
@@ -1293,6 +1293,7 @@ export function getProviderTypeOptions(category) {
|
||||
return (
|
||||
[
|
||||
{id: "Aliyun SMS", name: "Alibaba Cloud SMS"},
|
||||
{id: "Alibaba Cloud PNVS SMS", name: "Alibaba Cloud PNVS SMS"},
|
||||
{id: "Amazon SNS", name: "Amazon SNS"},
|
||||
{id: "Azure ACS", name: "Azure ACS"},
|
||||
{id: "Custom HTTP SMS", name: "Custom HTTP SMS"},
|
||||
@@ -2231,7 +2232,7 @@ export function createFormAndSubmit(url, params) {
|
||||
export function getFormTypeOptions() {
|
||||
return [
|
||||
{id: "users", name: "general:Users"},
|
||||
{id: "providers", name: "general:Providers"},
|
||||
{id: "providers", name: "application:Providers"},
|
||||
{id: "applications", name: "general:Applications"},
|
||||
{id: "organizations", name: "general:Organizations"},
|
||||
];
|
||||
@@ -2263,7 +2264,7 @@ export function getFormTypeItems(formType) {
|
||||
{name: "createdTime", label: "general:Created time", visible: true, width: "180"},
|
||||
{name: "displayName", label: "general:Display name", visible: true, width: "150"},
|
||||
{name: "category", label: "provider:Category", visible: true, width: "110"},
|
||||
{name: "type", label: "provider:Type", visible: true, width: "110"},
|
||||
{name: "type", label: "general:Type", visible: true, width: "110"},
|
||||
{name: "clientId", label: "provider:Client ID", visible: true, width: "100"},
|
||||
{name: "providerUrl", label: "provider:Provider URL", visible: true, width: "150"},
|
||||
];
|
||||
@@ -2274,7 +2275,7 @@ export function getFormTypeItems(formType) {
|
||||
{name: "displayName", label: "general:Display name", visible: true, width: "150"},
|
||||
{name: "logo", label: "Logo", visible: true, width: "200"},
|
||||
{name: "organization", label: "general:Organization", visible: true, width: "150"},
|
||||
{name: "providers", label: "general:Providers", visible: true, width: "500"},
|
||||
{name: "providers", label: "application:Providers", visible: true, width: "500"},
|
||||
];
|
||||
} else if (formType === "organizations") {
|
||||
return [
|
||||
|
||||
@@ -287,7 +287,7 @@ class SubscriptionEditPage extends React.Component {
|
||||
this.updateSubscriptionField("state", value);
|
||||
})}
|
||||
options={[
|
||||
{value: "Pending", name: i18next.t("subscription:Pending")},
|
||||
{value: "Pending", name: i18next.t("permission:Pending")},
|
||||
{value: "Active", name: i18next.t("subscription:Active")},
|
||||
{value: "Upcoming", name: i18next.t("subscription:Upcoming")},
|
||||
{value: "Expired", name: i18next.t("subscription:Expired")},
|
||||
|
||||
@@ -131,14 +131,14 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Period"),
|
||||
title: i18next.t("plan:Period"),
|
||||
dataIndex: "period",
|
||||
key: "period",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("period"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Start time"),
|
||||
title: i18next.t("subscription:Start time"),
|
||||
dataIndex: "startTime",
|
||||
key: "startTime",
|
||||
width: "140px",
|
||||
@@ -148,7 +148,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:End time"),
|
||||
title: i18next.t("subscription:End time"),
|
||||
dataIndex: "endTime",
|
||||
key: "endTime",
|
||||
width: "140px",
|
||||
@@ -165,7 +165,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("plan"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/plans/${record.owner}/${text}`}>
|
||||
<Link to={`/plans/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
@@ -179,7 +179,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("user"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/users/${record.owner}/${text}`}>
|
||||
<Link to={`/users/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
@@ -193,7 +193,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("payment"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/payments/${record.owner}/${text}`}>
|
||||
<Link to={`/payments/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
@@ -209,7 +209,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
render: (text, record, index) => {
|
||||
switch (text) {
|
||||
case "Pending":
|
||||
return Setting.getTag("processing", i18next.t("subscription:Pending"), <ExclamationCircleOutlined />);
|
||||
return Setting.getTag("processing", i18next.t("permission:Pending"), <ExclamationCircleOutlined />);
|
||||
case "Active":
|
||||
return Setting.getTag("success", i18next.t("subscription:Active"), <SyncOutlined spin />);
|
||||
case "Upcoming":
|
||||
|
||||
@@ -753,7 +753,7 @@ class SyncerEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.type} onChange={(value => {
|
||||
@@ -906,7 +906,7 @@ class SyncerEditPage extends React.Component {
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.database} onChange={e => {
|
||||
|
||||
@@ -147,7 +147,7 @@ class SyncerListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "100px",
|
||||
|
||||
@@ -203,7 +203,7 @@ class TicketEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Content")}:
|
||||
{i18next.t("provider:Content")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<TextArea autoSize={{minRows: 3, maxRows: 10}} value={this.state.ticket.content} disabled={!isAdmin && !isOwner} onChange={e => {
|
||||
|
||||
@@ -158,7 +158,7 @@ class TokenEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.token.scope} onChange={e => {
|
||||
|
||||
@@ -283,7 +283,7 @@ class TransactionEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("payment:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.type} onChange={e => {
|
||||
@@ -313,7 +313,7 @@ class TransactionEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("transaction:Tag - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{isRechargeMode ? (
|
||||
@@ -359,7 +359,7 @@ class TransactionEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("transaction:Amount"), i18next.t("transaction:Amount - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("product:Amount"), i18next.t("transaction:Amount - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={4} >
|
||||
<InputNumber disabled={!isRechargeMode} value={this.state.transaction.amount ?? 0} onChange={value => {
|
||||
@@ -369,7 +369,7 @@ class TransactionEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("currency:Currency"), i18next.t("currency:Currency - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.transaction.currency} disabled={!isRechargeMode} onChange={(value => {
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag, Tooltip} from "antd";
|
||||
import {
|
||||
Button, Card, Col, Form, Input, InputNumber, Layout, List,
|
||||
Menu, Result, Row, Select, Space, Spin, Switch, Tabs, Tag, Tooltip
|
||||
} from "antd";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import {TotpMfaType} from "./auth/MfaSetupPage";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
@@ -46,6 +49,8 @@ import MfaAccountTable from "./table/MfaAccountTable";
|
||||
import MfaTable from "./table/MfaTable";
|
||||
import TransactionTable from "./table/TransactionTable";
|
||||
import * as TransactionBackend from "./backend/TransactionBackend";
|
||||
import {Content, Header} from "antd/es/layout/layout";
|
||||
import Sider from "antd/es/layout/Sider";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@@ -67,6 +72,8 @@ class UserEditPage extends React.Component {
|
||||
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
|
||||
openFaceRecognitionModal: false,
|
||||
transactions: [],
|
||||
activeMenuKey: window.location.hash?.slice(1) || "",
|
||||
menuMode: "Horizontal",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,6 +182,7 @@ class UserEditPage extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
menuMode: res.data?.organizationObj?.accountMenu ?? "Horizontal",
|
||||
application: res.data,
|
||||
});
|
||||
});
|
||||
@@ -618,7 +626,7 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Title"), i18next.t("general:Title - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.user.title} onChange={e => {
|
||||
@@ -678,7 +686,7 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Real name"), i18next.t("user:Real name - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("application:Real name"), i18next.t("user:Real name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.user.realName} disabled={disabled} onChange={e => {
|
||||
@@ -736,7 +744,7 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
@@ -827,7 +835,7 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Balance credit"), i18next.t("user:Balance credit - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("organization:Balance credit"), i18next.t("organization:Balance credit - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.user.balanceCredit ?? 0} onChange={value => {
|
||||
@@ -840,7 +848,7 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Balance currency"), i18next.t("user:Balance currency - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("organization:Balance currency"), i18next.t("organization:Balance currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.user.balanceCurrency || "USD"} onChange={(value => {
|
||||
@@ -1333,6 +1341,152 @@ class UserEditPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
isAccountItemVisible(item) {
|
||||
if (!item.visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAdmin = Setting.isLocalAdminUser(this.props.account);
|
||||
if (item.viewRule === "Self") {
|
||||
if (!this.isSelfOrAdmin()) {
|
||||
return false;
|
||||
}
|
||||
} else if (item.viewRule === "Admin") {
|
||||
if (!isAdmin) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getAccountItemsByTab(tab) {
|
||||
const accountItems = this.getUserOrganization()?.accountItems || [];
|
||||
return accountItems.filter(item => {
|
||||
if (!this.isAccountItemVisible(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const itemTab = item.tab || "";
|
||||
return itemTab === tab;
|
||||
});
|
||||
}
|
||||
|
||||
getUniqueTabs() {
|
||||
const accountItems = this.getUserOrganization()?.accountItems || [];
|
||||
const tabs = new Set();
|
||||
|
||||
accountItems.forEach(item => {
|
||||
if (this.isAccountItemVisible(item)) {
|
||||
tabs.add(item.tab || "");
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(tabs).sort((a, b) => {
|
||||
// Empty string (default tab) comes first
|
||||
if (a === "") {
|
||||
return -1;
|
||||
}
|
||||
if (b === "") {
|
||||
return 1;
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
|
||||
renderUserForm() {
|
||||
const tabs = this.getUniqueTabs();
|
||||
|
||||
// If there are no tabs or only one tab (default), render without tab navigation
|
||||
if (tabs.length === 0 || (tabs.length === 1 && tabs[0] === "")) {
|
||||
const accountItems = this.getAccountItemsByTab("");
|
||||
return (
|
||||
<Form>
|
||||
{accountItems.map(accountItem => (
|
||||
<React.Fragment key={accountItem.name}>
|
||||
<Form.Item name={accountItem.name}
|
||||
validateTrigger="onChange"
|
||||
rules={[
|
||||
{
|
||||
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
|
||||
message: i18next.t("user:This field value doesn't match the pattern rule"),
|
||||
},
|
||||
]}
|
||||
style={{margin: 0}}>
|
||||
{this.renderAccountItem(accountItem)}
|
||||
</Form.Item>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
// Render with tabs
|
||||
const activeKey = this.state.activeMenuKey || tabs[0] || "";
|
||||
|
||||
return (
|
||||
<Layout style={{background: "inherit"}}>
|
||||
{
|
||||
this.state.menuMode === "Vertical" ? null : (
|
||||
<Header style={{background: "inherit", padding: "0px"}}>
|
||||
<Tabs
|
||||
onChange={(key) => {
|
||||
this.setState({activeMenuKey: key});
|
||||
window.location.hash = key;
|
||||
}}
|
||||
type="card"
|
||||
activeKey={activeKey}
|
||||
items={tabs.map(tab => ({
|
||||
label: tab === "" ? i18next.t("general:Default") : tab,
|
||||
key: tab,
|
||||
}))}
|
||||
/>
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
<Layout style={{background: "inherit", maxHeight: "70vh", overflow: "auto"}}>
|
||||
{
|
||||
this.state.menuMode === "Vertical" ? (
|
||||
<Sider width={200} style={{background: "inherit", position: "sticky", top: 0}}>
|
||||
<Menu
|
||||
mode="vertical"
|
||||
selectedKeys={[activeKey]}
|
||||
onClick={({key}) => {
|
||||
this.setState({activeMenuKey: key});
|
||||
window.location.hash = key;
|
||||
}}
|
||||
style={{marginBottom: "20px", height: "100%"}}
|
||||
items={tabs.map(tab => ({
|
||||
label: tab === "" ? i18next.t("general:Default") : tab,
|
||||
key: tab,
|
||||
}))}
|
||||
/>
|
||||
</Sider>) : null
|
||||
}
|
||||
<Content style={{padding: "15px"}}>
|
||||
<Form>
|
||||
{this.getAccountItemsByTab(activeKey).map(accountItem => (
|
||||
<React.Fragment key={accountItem.name}>
|
||||
<Form.Item name={accountItem.name}
|
||||
validateTrigger="onChange"
|
||||
rules={[
|
||||
{
|
||||
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
|
||||
message: i18next.t("user:This field value doesn't match the pattern rule"),
|
||||
},
|
||||
]}
|
||||
style={{margin: 0}}>
|
||||
{this.renderAccountItem(accountItem)}
|
||||
</Form.Item>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
renderUser() {
|
||||
return (
|
||||
<div>
|
||||
@@ -1346,42 +1500,7 @@ class UserEditPage extends React.Component {
|
||||
</div>
|
||||
)
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Form>
|
||||
{
|
||||
this.getUserOrganization()?.accountItems?.map(accountItem => {
|
||||
if (!accountItem.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdmin = Setting.isLocalAdminUser(this.props.account);
|
||||
|
||||
if (accountItem.viewRule === "Self") {
|
||||
if (!this.isSelfOrAdmin()) {
|
||||
return null;
|
||||
}
|
||||
} else if (accountItem.viewRule === "Admin") {
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={accountItem.name}>
|
||||
<Form.Item name={accountItem.name}
|
||||
validateTrigger="onChange"
|
||||
rules={[
|
||||
{
|
||||
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
|
||||
message: i18next.t("user:This field value doesn't match the pattern rule"),
|
||||
},
|
||||
]}
|
||||
style={{margin: 0}}>
|
||||
{this.renderAccountItem(accountItem)}
|
||||
</Form.Item>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Form>
|
||||
{this.renderUserForm()}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -191,7 +191,7 @@ class UserListPage extends BaseListPage {
|
||||
impersonateUser(user) {
|
||||
UserBackend.impersonateUser(user).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Success"));
|
||||
Setting.showMessage("success", i18next.t("general:Successfully executed"));
|
||||
Setting.goToLinkSoft(this, "/");
|
||||
window.location.reload();
|
||||
} else {
|
||||
@@ -393,7 +393,7 @@ class UserListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("affiliation"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Real name"),
|
||||
title: i18next.t("application:Real name"),
|
||||
dataIndex: "realName",
|
||||
key: "realName",
|
||||
width: "120px",
|
||||
@@ -479,7 +479,7 @@ class UserListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Balance credit"),
|
||||
title: i18next.t("organization:Balance credit"),
|
||||
dataIndex: "balanceCredit",
|
||||
key: "balanceCredit",
|
||||
width: "120px",
|
||||
@@ -489,7 +489,7 @@ class UserListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Balance currency"),
|
||||
title: i18next.t("organization:Balance currency"),
|
||||
dataIndex: "balanceCurrency",
|
||||
key: "balanceCurrency",
|
||||
width: "140px",
|
||||
|
||||
@@ -73,7 +73,7 @@ class VerificationListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "90px",
|
||||
|
||||
@@ -220,7 +220,7 @@ class WebhookEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("webhook:Method - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.webhook.method} onChange={(value => {this.updateWebhookField("method", value);})}>
|
||||
|
||||
@@ -25,8 +25,8 @@ export function getAccount(query = "") {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function signup(values) {
|
||||
return fetch(`${authConfig.serverUrl}/api/signup`, {
|
||||
export function signup(values, oAuthParams) {
|
||||
return fetch(`${authConfig.serverUrl}/api/signup${oAuthParamsToQuery(oAuthParams)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(values),
|
||||
|
||||
@@ -44,7 +44,7 @@ class CasLogout extends React.Component {
|
||||
if (logoutRes.status === "ok") {
|
||||
logoutTimeOut(logoutRes.data2);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("login:Failed to log out")}: ${logoutRes.msg}`);
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to log out")}: ${logoutRes.msg}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -67,7 +67,7 @@ class CasLogout extends React.Component {
|
||||
if (res.status === "ok") {
|
||||
logoutTimeOut(res.data2);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("login:Failed to log out")}: ${res.msg}`);
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to log out")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user