mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-16 10:43:35 +08:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
df61a536c1 | |||
47da3cdaa0 | |||
8d246f2d98 | |||
44cd55e55f | |||
6b42d35223 | |||
c84150cede | |||
de2689ac39 | |||
88c0856d17 | |||
319031da28 | |||
d20f3eb039 | |||
3e13e61d8f | |||
1260354b36 | |||
af79fdedf2 | |||
02333f2f0c | |||
79bd58e0e6 | |||
de73ff0e60 | |||
a9d662f1bd | |||
65dcbd2236 | |||
6455734807 | |||
2eefeaffa7 | |||
04eaad1c80 | |||
9f084a0799 | |||
293b9f1036 | |||
437376c472 | |||
cc528c5d8c | |||
54e2055ffb | |||
983a30a2e0 | |||
37d0157d41 | |||
d4dc236770 | |||
596742d782 | |||
ce921c00cd | |||
3830e443b0 | |||
9092cad631 | |||
0b5ecca5c8 | |||
3d9b305bbb | |||
0217e359e7 | |||
695a612e77 | |||
645d53e2c6 | |||
73b9d73f64 |
@ -1,11 +1,11 @@
|
||||
FROM node:16.13.0 AS FRONT
|
||||
FROM node:16.18.0 AS FRONT
|
||||
WORKDIR /web
|
||||
COPY ./web .
|
||||
RUN yarn config set registry https://registry.npmmirror.com
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
|
||||
|
||||
|
||||
FROM golang:1.17.5 AS BACK
|
||||
FROM golang:1.19.9 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
COPY . .
|
||||
RUN ./build.sh
|
||||
|
@ -15,13 +15,13 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
xormadapter "github.com/casdoor/xorm-adapter/v3"
|
||||
stringadapter "github.com/qiangmzsx/string-adapter/v2"
|
||||
)
|
||||
@ -88,6 +88,7 @@ p, *, *, GET, /api/logout, *, *
|
||||
p, *, *, GET, /api/get-account, *, *
|
||||
p, *, *, GET, /api/userinfo, *, *
|
||||
p, *, *, GET, /api/user, *, *
|
||||
p, *, *, GET, /api/health, *, *
|
||||
p, *, *, POST, /api/webhook, *, *
|
||||
p, *, *, GET, /api/get-webhook-event, *, *
|
||||
p, *, *, GET, /api/get-captcha-status, *, *
|
||||
@ -123,6 +124,9 @@ p, *, *, GET, /api/get-release, *, *
|
||||
p, *, *, GET, /api/get-default-application, *, *
|
||||
p, *, *, GET, /api/get-prometheus-info, *, *
|
||||
p, *, *, *, /api/metrics, *, *
|
||||
p, *, *, GET, /api/get-subscriptions, *, *
|
||||
p, *, *, GET, /api/get-pricing, *, *
|
||||
p, *, *, GET, /api/get-plan, *, *
|
||||
`
|
||||
|
||||
sa := stringadapter.NewAdapter(ruleText)
|
||||
@ -149,8 +153,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
}
|
||||
}
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", subOwner, subName)
|
||||
user := object.GetUser(userId)
|
||||
user := object.GetUser(util.GetId(subOwner, subName))
|
||||
if user != nil && user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
|
||||
return true
|
||||
}
|
||||
|
@ -20,5 +20,4 @@ staticBaseUrl = "https://cdn.casbin.org"
|
||||
isDemoMode = false
|
||||
batchSize = 100
|
||||
ldapServerPort = 389
|
||||
languages = en,zh,es,fr,de,id,ja,ko,ru,vi
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
|
@ -112,13 +112,8 @@ func GetLanguage(language string) string {
|
||||
|
||||
if len(language) < 2 {
|
||||
return "en"
|
||||
}
|
||||
|
||||
language = language[0:2]
|
||||
if strings.Contains(GetConfigString("languages"), language) {
|
||||
return language
|
||||
} else {
|
||||
return "en"
|
||||
return language[0:2]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ func (c *ApiController) Signup() {
|
||||
return
|
||||
}
|
||||
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", authForm.Organization))
|
||||
organization := object.GetOrganization(util.GetId("admin", authForm.Organization))
|
||||
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
@ -126,7 +126,7 @@ func (c *ApiController) Signup() {
|
||||
username = id
|
||||
}
|
||||
|
||||
initScore, err := getInitScore(organization)
|
||||
initScore, err := organization.GetInitScore()
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
|
||||
return
|
||||
@ -189,6 +189,11 @@ func (c *ApiController) Signup() {
|
||||
object.DisableVerificationCode(authForm.Email)
|
||||
object.DisableVerificationCode(checkPhone)
|
||||
|
||||
isSignupFromPricing := authForm.Plan != "" && authForm.Pricing != ""
|
||||
if isSignupFromPricing {
|
||||
object.Subscribe(organization.Name, user.Name, authForm.Plan, authForm.Pricing)
|
||||
}
|
||||
|
||||
record := object.NewRecord(c.Ctx)
|
||||
record.Organization = application.Organization
|
||||
record.User = user.Name
|
||||
|
@ -312,6 +312,11 @@ func (c *ApiController) Login() {
|
||||
|
||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||
|
||||
organization := object.GetOrganizationByUser(user)
|
||||
if user != nil && organization.HasRequiredMfa() && !user.IsMfaEnabled() {
|
||||
resp.Msg = object.RequiredMfa
|
||||
}
|
||||
|
||||
record := object.NewRecord(c.Ctx)
|
||||
record.Organization = application.Organization
|
||||
record.User = user.Name
|
||||
@ -330,7 +335,7 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", application.Organization))
|
||||
organization := object.GetOrganization(util.GetId("admin", application.Organization))
|
||||
provider := object.GetProvider(util.GetId("admin", authForm.Provider))
|
||||
providerItem := application.GetProviderItem(provider.Name)
|
||||
if !providerItem.IsProviderVisible() {
|
||||
@ -391,7 +396,7 @@ func (c *ApiController) Login() {
|
||||
if authForm.Method == "signup" {
|
||||
user := &object.User{}
|
||||
if provider.Category == "SAML" {
|
||||
user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id))
|
||||
user = object.GetUser(util.GetId(application.Organization, userInfo.Id))
|
||||
} else if provider.Category == "OAuth" {
|
||||
user = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
|
||||
}
|
||||
@ -411,24 +416,31 @@ func (c *ApiController) Login() {
|
||||
util.SafeGoroutine(func() { object.AddRecord(record) })
|
||||
} else if provider.Category == "OAuth" {
|
||||
// Sign up via OAuth
|
||||
if !application.EnableSignUp {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support"), provider.Type, userInfo.Username, userInfo.DisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
if !providerItem.CanSignUp {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up"), provider.Type, userInfo.Username, userInfo.DisplayName, provider.Type))
|
||||
return
|
||||
}
|
||||
|
||||
if application.EnableLinkWithEmail {
|
||||
// find user that has the same email
|
||||
user = object.GetUserByField(application.Organization, "email", userInfo.Email)
|
||||
if userInfo.Email != "" {
|
||||
// Find existing user with Email
|
||||
user = object.GetUserByField(application.Organization, "email", userInfo.Email)
|
||||
}
|
||||
|
||||
if user == nil && userInfo.Phone != "" {
|
||||
// Find existing user with phone number
|
||||
user = object.GetUserByField(application.Organization, "phone", userInfo.Phone)
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil || user.IsDeleted {
|
||||
if !application.EnableSignUp {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support"), provider.Type, userInfo.Username, userInfo.DisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
if !providerItem.CanSignUp {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up"), provider.Type, userInfo.Username, userInfo.DisplayName, provider.Type))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle username conflicts
|
||||
tmpUser := object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Username))
|
||||
tmpUser := object.GetUser(util.GetId(application.Organization, userInfo.Username))
|
||||
if tmpUser != nil {
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
@ -442,7 +454,7 @@ func (c *ApiController) Login() {
|
||||
|
||||
properties := map[string]string{}
|
||||
properties["no"] = strconv.Itoa(object.GetUserCount(application.Organization, "", "") + 2)
|
||||
initScore, err := getInitScore(organization)
|
||||
initScore, err := organization.GetInitScore()
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
|
||||
return
|
||||
|
@ -48,7 +48,7 @@ func (c *ApiController) IsGlobalAdmin() bool {
|
||||
|
||||
func (c *ApiController) IsAdmin() bool {
|
||||
isGlobalAdmin, user := c.isGlobalAdmin()
|
||||
if user == nil {
|
||||
if !isGlobalAdmin && user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
func (c *ApiController) Enforce() {
|
||||
permissionId := c.Input().Get("permissionId")
|
||||
modelId := c.Input().Get("modelId")
|
||||
resourceId := c.Input().Get("resourceId")
|
||||
|
||||
var request object.CasbinRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
|
||||
@ -35,17 +36,24 @@ func (c *ApiController) Enforce() {
|
||||
if permissionId != "" {
|
||||
c.Data["json"] = object.Enforce(permissionId, &request)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
owner, modelName := util.GetOwnerAndNameFromId(modelId)
|
||||
permissions := object.GetPermissionsByModel(owner, modelName)
|
||||
|
||||
res := []bool{}
|
||||
for _, permission := range permissions {
|
||||
res = append(res, object.Enforce(permission.GetId(), &request))
|
||||
}
|
||||
c.Data["json"] = res
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
permissions := make([]*object.Permission, 0)
|
||||
res := []bool{}
|
||||
|
||||
if modelId != "" {
|
||||
owner, modelName := util.GetOwnerAndNameFromId(modelId)
|
||||
permissions = object.GetPermissionsByModel(owner, modelName)
|
||||
} else {
|
||||
permissions = object.GetPermissionsByResource(resourceId)
|
||||
}
|
||||
|
||||
for _, permission := range permissions {
|
||||
res = append(res, object.Enforce(permission.GetId(), &request))
|
||||
}
|
||||
c.Data["json"] = res
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) BatchEnforce() {
|
||||
|
@ -23,7 +23,8 @@ import (
|
||||
|
||||
type LdapResp struct {
|
||||
// Groups []LdapRespGroup `json:"groups"`
|
||||
Users []object.LdapRespUser `json:"users"`
|
||||
Users []object.LdapUser `json:"users"`
|
||||
ExistUuids []string `json:"existUuids"`
|
||||
}
|
||||
|
||||
//type LdapRespGroup struct {
|
||||
@ -32,8 +33,8 @@ type LdapResp struct {
|
||||
//}
|
||||
|
||||
type LdapSyncResp struct {
|
||||
Exist []object.LdapRespUser `json:"exist"`
|
||||
Failed []object.LdapRespUser `json:"failed"`
|
||||
Exist []object.LdapUser `json:"exist"`
|
||||
Failed []object.LdapUser `json:"failed"`
|
||||
}
|
||||
|
||||
// GetLdapUsers
|
||||
@ -71,27 +72,17 @@ func (c *ApiController) GetLdapUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
var resp LdapResp
|
||||
uuids := make([]string, len(users))
|
||||
for _, user := range users {
|
||||
resp.Users = append(resp.Users, object.LdapRespUser{
|
||||
UidNumber: user.UidNumber,
|
||||
Uid: user.Uid,
|
||||
Cn: user.Cn,
|
||||
GroupId: user.GidNumber,
|
||||
// GroupName: groupsMap[user.GidNumber].Cn,
|
||||
Uuid: user.Uuid,
|
||||
DisplayName: user.DisplayName,
|
||||
Email: util.GetMaxLenStr(user.Mail, user.Email, user.EmailAddress),
|
||||
Phone: util.GetMaxLenStr(user.TelephoneNumber, user.Mobile, user.MobileTelephoneNumber),
|
||||
Address: util.GetMaxLenStr(user.RegisteredAddress, user.PostalAddress),
|
||||
})
|
||||
uuids = append(uuids, user.Uuid)
|
||||
for i, user := range users {
|
||||
uuids[i] = user.GetLdapUuid()
|
||||
}
|
||||
|
||||
existUuids := object.GetExistUuids(ldapServer.Owner, uuids)
|
||||
|
||||
c.ResponseOk(resp, existUuids)
|
||||
resp := LdapResp{
|
||||
Users: object.AutoAdjustLdapUser(users),
|
||||
ExistUuids: existUuids,
|
||||
}
|
||||
c.ResponseOk(resp)
|
||||
}
|
||||
|
||||
// GetLdaps
|
||||
@ -206,7 +197,7 @@ func (c *ApiController) DeleteLdap() {
|
||||
func (c *ApiController) SyncLdapUsers() {
|
||||
owner := c.Input().Get("owner")
|
||||
ldapId := c.Input().Get("ldapId")
|
||||
var users []object.LdapRespUser
|
||||
var users []object.LdapUser
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@ -215,10 +206,10 @@ func (c *ApiController) SyncLdapUsers() {
|
||||
|
||||
object.UpdateLdapSyncTime(ldapId)
|
||||
|
||||
exist, failed := object.SyncLdapUsers(owner, users, ldapId)
|
||||
exist, failed, _ := object.SyncLdapUsers(owner, users, ldapId)
|
||||
|
||||
c.ResponseOk(&LdapSyncResp{
|
||||
Exist: *exist,
|
||||
Failed: *failed,
|
||||
Exist: exist,
|
||||
Failed: failed,
|
||||
})
|
||||
}
|
||||
|
137
controllers/plan.go
Normal file
137
controllers/plan.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetPlans
|
||||
// @Title GetPlans
|
||||
// @Tag Plan API
|
||||
// @Description get plans
|
||||
// @Param owner query string true "The owner of plans"
|
||||
// @Success 200 {array} object.Plan The Response object
|
||||
// @router /get-plans [get]
|
||||
func (c *ApiController) GetPlans() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
if limit == "" || page == "" {
|
||||
c.Data["json"] = object.GetPlans(owner)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetPlanCount(owner, field, value)))
|
||||
plan := object.GetPaginatedPlans(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
c.ResponseOk(plan, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlan
|
||||
// @Title GetPlan
|
||||
// @Tag Plan API
|
||||
// @Description get plan
|
||||
// @Param id query string true "The id ( owner/name ) of the plan"
|
||||
// @Param includeOption query bool false "Should include plan's option"
|
||||
// @Success 200 {object} object.Plan The Response object
|
||||
// @router /get-plan [get]
|
||||
func (c *ApiController) GetPlan() {
|
||||
id := c.Input().Get("id")
|
||||
includeOption := c.Input().Get("includeOption") == "true"
|
||||
|
||||
plan := object.GetPlan(id)
|
||||
|
||||
if includeOption {
|
||||
options := object.GetPermissionsByRole(plan.Role)
|
||||
|
||||
for _, option := range options {
|
||||
plan.Options = append(plan.Options, option.DisplayName)
|
||||
}
|
||||
|
||||
c.Data["json"] = plan
|
||||
} else {
|
||||
c.Data["json"] = plan
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdatePlan
|
||||
// @Title UpdatePlan
|
||||
// @Tag Plan API
|
||||
// @Description update plan
|
||||
// @Param id query string true "The id ( owner/name ) of the plan"
|
||||
// @Param body body object.Plan true "The details of the plan"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-plan [post]
|
||||
func (c *ApiController) UpdatePlan() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var plan object.Plan
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddPlan
|
||||
// @Title AddPlan
|
||||
// @Tag Plan API
|
||||
// @Description add plan
|
||||
// @Param body body object.Plan true "The details of the plan"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-plan [post]
|
||||
func (c *ApiController) AddPlan() {
|
||||
var plan object.Plan
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddPlan(&plan))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeletePlan
|
||||
// @Title DeletePlan
|
||||
// @Tag Plan API
|
||||
// @Description delete plan
|
||||
// @Param body body object.Plan true "The details of the plan"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-plan [post]
|
||||
func (c *ApiController) DeletePlan() {
|
||||
var plan object.Plan
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeletePlan(&plan))
|
||||
c.ServeJSON()
|
||||
}
|
125
controllers/pricing.go
Normal file
125
controllers/pricing.go
Normal file
@ -0,0 +1,125 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetPricings
|
||||
// @Title GetPricings
|
||||
// @Tag Pricing API
|
||||
// @Description get pricings
|
||||
// @Param owner query string true "The owner of pricings"
|
||||
// @Success 200 {array} object.Pricing The Response object
|
||||
// @router /get-pricings [get]
|
||||
func (c *ApiController) GetPricings() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
if limit == "" || page == "" {
|
||||
c.Data["json"] = object.GetPricings(owner)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetPricingCount(owner, field, value)))
|
||||
pricing := object.GetPaginatedPricings(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
c.ResponseOk(pricing, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetPricing
|
||||
// @Title GetPricing
|
||||
// @Tag Pricing API
|
||||
// @Description get pricing
|
||||
// @Param id query string true "The id ( owner/name ) of the pricing"
|
||||
// @Success 200 {object} object.pricing The Response object
|
||||
// @router /get-pricing [get]
|
||||
func (c *ApiController) GetPricing() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
pricing := object.GetPricing(id)
|
||||
|
||||
c.Data["json"] = pricing
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdatePricing
|
||||
// @Title UpdatePricing
|
||||
// @Tag Pricing API
|
||||
// @Description update pricing
|
||||
// @Param id query string true "The id ( owner/name ) of the pricing"
|
||||
// @Param body body object.Pricing true "The details of the pricing"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-pricing [post]
|
||||
func (c *ApiController) UpdatePricing() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var pricing object.Pricing
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &pricing)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdatePricing(id, &pricing))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddPricing
|
||||
// @Title AddPricing
|
||||
// @Tag Pricing API
|
||||
// @Description add pricing
|
||||
// @Param body body object.Pricing true "The details of the pricing"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-pricing [post]
|
||||
func (c *ApiController) AddPricing() {
|
||||
var pricing object.Pricing
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &pricing)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddPricing(&pricing))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeletePricing
|
||||
// @Title DeletePricing
|
||||
// @Tag Pricing API
|
||||
// @Description delete pricing
|
||||
// @Param body body object.Pricing true "The details of the pricing"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-pricing [post]
|
||||
func (c *ApiController) DeletePricing() {
|
||||
var pricing object.Pricing
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &pricing)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeletePricing(&pricing))
|
||||
c.ServeJSON()
|
||||
}
|
@ -37,13 +37,19 @@ func (c *ApiController) GetProviders() {
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if limit == "" || page == "" {
|
||||
c.Data["json"] = object.GetMaskedProviders(object.GetProviders(owner))
|
||||
c.Data["json"] = object.GetMaskedProviders(object.GetProviders(owner), isMaskEnabled)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetProviderCount(owner, field, value)))
|
||||
providers := object.GetMaskedProviders(object.GetPaginationProviders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
providers := object.GetMaskedProviders(object.GetPaginationProviders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder), isMaskEnabled)
|
||||
c.ResponseOk(providers, paginator.Nums())
|
||||
}
|
||||
}
|
||||
@ -61,13 +67,19 @@ func (c *ApiController) GetGlobalProviders() {
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if limit == "" || page == "" {
|
||||
c.Data["json"] = object.GetMaskedProviders(object.GetGlobalProviders())
|
||||
c.Data["json"] = object.GetMaskedProviders(object.GetGlobalProviders(), isMaskEnabled)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetGlobalProviderCount(field, value)))
|
||||
providers := object.GetMaskedProviders(object.GetPaginationGlobalProviders(paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
providers := object.GetMaskedProviders(object.GetPaginationGlobalProviders(paginator.Offset(), limit, field, value, sortField, sortOrder), isMaskEnabled)
|
||||
c.ResponseOk(providers, paginator.Nums())
|
||||
}
|
||||
}
|
||||
@ -81,7 +93,13 @@ func (c *ApiController) GetGlobalProviders() {
|
||||
// @router /get-provider [get]
|
||||
func (c *ApiController) GetProvider() {
|
||||
id := c.Input().Get("id")
|
||||
c.Data["json"] = object.GetMaskedProvider(object.GetProvider(id))
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = object.GetMaskedProvider(object.GetProvider(id), isMaskEnabled)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
125
controllers/subscription.go
Normal file
125
controllers/subscription.go
Normal file
@ -0,0 +1,125 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetSubscriptions
|
||||
// @Title GetSubscriptions
|
||||
// @Tag Subscription API
|
||||
// @Description get subscriptions
|
||||
// @Param owner query string true "The owner of subscriptions"
|
||||
// @Success 200 {array} object.Subscription The Response object
|
||||
// @router /get-subscriptions [get]
|
||||
func (c *ApiController) GetSubscriptions() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
if limit == "" || page == "" {
|
||||
c.Data["json"] = object.GetSubscriptions(owner)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetSubscriptionCount(owner, field, value)))
|
||||
subscription := object.GetPaginationSubscriptions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
c.ResponseOk(subscription, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetSubscription
|
||||
// @Title GetSubscription
|
||||
// @Tag Subscription API
|
||||
// @Description get subscription
|
||||
// @Param id query string true "The id ( owner/name ) of the subscription"
|
||||
// @Success 200 {object} object.subscription The Response object
|
||||
// @router /get-subscription [get]
|
||||
func (c *ApiController) GetSubscription() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
subscription := object.GetSubscription(id)
|
||||
|
||||
c.Data["json"] = subscription
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateSubscription
|
||||
// @Title UpdateSubscription
|
||||
// @Tag Subscription API
|
||||
// @Description update subscription
|
||||
// @Param id query string true "The id ( owner/name ) of the subscription"
|
||||
// @Param body body object.Subscription true "The details of the subscription"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-subscription [post]
|
||||
func (c *ApiController) UpdateSubscription() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var subscription object.Subscription
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &subscription)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSubscription(id, &subscription))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddSubscription
|
||||
// @Title AddSubscription
|
||||
// @Tag Subscription API
|
||||
// @Description add subscription
|
||||
// @Param body body object.Subscription true "The details of the subscription"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-subscription [post]
|
||||
func (c *ApiController) AddSubscription() {
|
||||
var subscription object.Subscription
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &subscription)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddSubscription(&subscription))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteSubscription
|
||||
// @Title DeleteSubscription
|
||||
// @Tag Subscription API
|
||||
// @Description delete subscription
|
||||
// @Param body body object.Subscription true "The details of the subscription"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-subscription [post]
|
||||
func (c *ApiController) DeleteSubscription() {
|
||||
var subscription object.Subscription
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &subscription)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSubscription(&subscription))
|
||||
c.ServeJSON()
|
||||
}
|
@ -59,3 +59,13 @@ func (c *ApiController) GetVersionInfo() {
|
||||
|
||||
c.ResponseOk(versionInfo)
|
||||
}
|
||||
|
||||
// Health
|
||||
// @Title Health
|
||||
// @Tag System API
|
||||
// @Description check if the system is live
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /health [get]
|
||||
func (c *ApiController) Health() {
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ func (c *ApiController) GetUsers() {
|
||||
// @Title GetUser
|
||||
// @Tag User API
|
||||
// @Description get user
|
||||
// @Param id query string true "The id ( owner/name ) of the user"
|
||||
// @Param id query string false "The id ( owner/name ) of the user"
|
||||
// @Param owner query string false "The owner of the user"
|
||||
// @Param email query string false "The email of the user"
|
||||
// @Param phone query string false "The phone of the user"
|
||||
@ -92,13 +92,19 @@ func (c *ApiController) GetUser() {
|
||||
email := c.Input().Get("email")
|
||||
phone := c.Input().Get("phone")
|
||||
userId := c.Input().Get("userId")
|
||||
|
||||
owner := c.Input().Get("owner")
|
||||
|
||||
var userFromUserId *object.User
|
||||
if userId != "" && owner != "" {
|
||||
userFromUserId = object.GetUserByUserId(owner, userId)
|
||||
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
|
||||
}
|
||||
|
||||
if owner == "" {
|
||||
owner = util.GetOwnerFromId(id)
|
||||
}
|
||||
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner))
|
||||
organization := object.GetOrganization(util.GetId("admin", owner))
|
||||
if !organization.IsProfilePublic {
|
||||
requestUserId := c.GetSessionUsername()
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
|
||||
@ -115,7 +121,7 @@ func (c *ApiController) GetUser() {
|
||||
case phone != "":
|
||||
user = object.GetUserByPhone(owner, phone)
|
||||
case userId != "":
|
||||
user = object.GetUserByUserId(owner, userId)
|
||||
user = userFromUserId
|
||||
default:
|
||||
user = object.GetUser(id)
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
@ -115,12 +114,25 @@ func (c *ApiController) RequireAdmin() (string, bool) {
|
||||
return user.Owner, true
|
||||
}
|
||||
|
||||
func getInitScore(organization *object.Organization) (int, error) {
|
||||
if organization != nil {
|
||||
return organization.InitScore, nil
|
||||
} else {
|
||||
return strconv.Atoi(conf.GetConfigString("initScore"))
|
||||
// IsMaskedEnabled ...
|
||||
func (c *ApiController) IsMaskedEnabled() (bool, bool) {
|
||||
isMaskEnabled := true
|
||||
withSecret := c.Input().Get("withSecret")
|
||||
if withSecret == "1" {
|
||||
isMaskEnabled = false
|
||||
|
||||
if conf.IsDemoMode() {
|
||||
c.ResponseError(c.T("general:this operation is not allowed in demo mode"))
|
||||
return false, isMaskEnabled
|
||||
}
|
||||
|
||||
_, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return false, isMaskEnabled
|
||||
}
|
||||
}
|
||||
|
||||
return true, isMaskEnabled
|
||||
}
|
||||
|
||||
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, *object.User, bool) {
|
||||
|
@ -54,4 +54,7 @@ type AuthForm struct {
|
||||
MfaType string `json:"mfaType"`
|
||||
Passcode string `json:"passcode"`
|
||||
RecoveryCode string `json:"recoveryCode"`
|
||||
|
||||
Plan string `json:"plan"`
|
||||
Pricing string `json:"pricing"`
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ func TestGenerateI18nFrontend(t *testing.T) {
|
||||
applyToOtherLanguage("frontend", "ko", data)
|
||||
applyToOtherLanguage("frontend", "ru", data)
|
||||
applyToOtherLanguage("frontend", "vi", data)
|
||||
applyToOtherLanguage("frontend", "pt", data)
|
||||
}
|
||||
|
||||
func TestGenerateI18nBackend(t *testing.T) {
|
||||
@ -47,4 +48,5 @@ func TestGenerateI18nBackend(t *testing.T) {
|
||||
applyToOtherLanguage("backend", "ko", data)
|
||||
applyToOtherLanguage("backend", "ru", data)
|
||||
applyToOtherLanguage("backend", "vi", data)
|
||||
applyToOtherLanguage("backend", "pt", data)
|
||||
}
|
||||
|
22
i18n/util.go
22
i18n/util.go
@ -73,23 +73,27 @@ func applyData(data1 *I18nData, data2 *I18nData) {
|
||||
}
|
||||
}
|
||||
|
||||
func Translate(lang string, error string) string {
|
||||
tokens := strings.SplitN(error, ":", 2)
|
||||
if !strings.Contains(error, ":") || len(tokens) != 2 {
|
||||
return "Translate Error: " + error
|
||||
func Translate(language string, errorText string) string {
|
||||
tokens := strings.SplitN(errorText, ":", 2)
|
||||
if !strings.Contains(errorText, ":") || len(tokens) != 2 {
|
||||
return fmt.Sprintf("Translate error: the error text doesn't contain \":\", errorText = %s", errorText)
|
||||
}
|
||||
|
||||
if langMap[lang] == nil {
|
||||
file, _ := f.ReadFile("locales/" + lang + "/data.json")
|
||||
if langMap[language] == nil {
|
||||
file, err := f.ReadFile(fmt.Sprintf("locales/%s/data.json", language))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Translate error: the language \"%s\" is not supported, err = %s", language, err.Error())
|
||||
}
|
||||
|
||||
data := I18nData{}
|
||||
err := util.JsonToStruct(string(file), &data)
|
||||
err = util.JsonToStruct(string(file), &data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
langMap[lang] = data
|
||||
langMap[language] = data
|
||||
}
|
||||
|
||||
res := langMap[lang][tokens[0]][tokens[1]]
|
||||
res := langMap[language][tokens[0]][tokens[1]]
|
||||
if res == "" {
|
||||
res = tokens[1]
|
||||
}
|
||||
|
@ -179,8 +179,12 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
corpEmail, jobNumber, err := idp.getUserCorpEmail(userId, corpAccessToken)
|
||||
corpMobile, corpEmail, jobNumber, err := idp.getUserCorpEmail(userId, corpAccessToken)
|
||||
if err == nil {
|
||||
if corpMobile != "" {
|
||||
userInfo.Phone = corpMobile
|
||||
}
|
||||
|
||||
if corpEmail != "" {
|
||||
userInfo.Email = corpEmail
|
||||
}
|
||||
@ -264,27 +268,29 @@ func (idp *DingTalkIdProvider) getUserId(unionId string, accessToken string) (st
|
||||
return data.Result.UserId, nil
|
||||
}
|
||||
|
||||
func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken string) (string, string, error) {
|
||||
func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken string) (string, string, string, error) {
|
||||
// https://open.dingtalk.com/document/isvapp/query-user-details
|
||||
body := make(map[string]string)
|
||||
body["userid"] = userId
|
||||
respBytes, err := idp.postWithBody(body, "https://oapi.dingtalk.com/topapi/v2/user/get?access_token="+accessToken)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
ErrMessage string `json:"errmsg"`
|
||||
Result struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
JobNumber string `json:"job_number"`
|
||||
} `json:"result"`
|
||||
}
|
||||
err = json.Unmarshal(respBytes, &data)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
if data.ErrMessage != "ok" {
|
||||
return "", "", fmt.Errorf(data.ErrMessage)
|
||||
return "", "", "", fmt.Errorf(data.ErrMessage)
|
||||
}
|
||||
return data.Result.Email, data.Result.JobNumber, nil
|
||||
return data.Result.Mobile, data.Result.Email, data.Result.JobNumber, nil
|
||||
}
|
||||
|
2
main.go
2
main.go
@ -59,8 +59,8 @@ func main() {
|
||||
beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter)
|
||||
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
|
||||
beego.InsertFilter("*", beego.BeforeRouter, routers.AuthzFilter)
|
||||
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
|
||||
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
|
||||
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
|
||||
|
||||
beego.BConfig.WebConfig.Session.SessionOn = true
|
||||
beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id"
|
||||
|
@ -240,6 +240,21 @@ func (a *Adapter) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Subscription))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Plan))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Pricing))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {
|
||||
|
@ -150,7 +150,7 @@ func getProviderMap(owner string) map[string]*Provider {
|
||||
UpdateProvider(provider.Owner+"/"+provider.Name, provider)
|
||||
}
|
||||
|
||||
m[provider.Name] = GetMaskedProvider(provider)
|
||||
m[provider.Name] = GetMaskedProvider(provider, true)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@ -326,6 +326,12 @@ func UpdateApplication(id string, application *Application) bool {
|
||||
}
|
||||
|
||||
func AddApplication(application *Application) bool {
|
||||
if application.Owner == "" {
|
||||
application.Owner = "admin"
|
||||
}
|
||||
if application.Organization == "" {
|
||||
application.Organization = "built-in"
|
||||
}
|
||||
if application.ClientId == "" {
|
||||
application.ClientId = util.GenerateClientId()
|
||||
}
|
||||
|
@ -134,6 +134,24 @@ func getCert(owner string, name string) *Cert {
|
||||
}
|
||||
}
|
||||
|
||||
func getCertByName(name string) *Cert {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert := Cert{Name: name}
|
||||
existed, err := adapter.Engine.Get(&cert)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &cert
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetCert(id string) *Cert {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getCert(owner, name)
|
||||
@ -189,7 +207,7 @@ func (p *Cert) GetId() string {
|
||||
|
||||
func getCertByApplication(application *Application) *Cert {
|
||||
if application.Cert != "" {
|
||||
return getCert("admin", application.Cert)
|
||||
return getCertByName(application.Cert)
|
||||
} else {
|
||||
return GetDefaultCert()
|
||||
}
|
||||
|
@ -175,7 +175,11 @@ func CheckPassword(user *User, password string, lang string, options ...bool) st
|
||||
return i18n.Translate(lang, "check:Organization does not exist")
|
||||
}
|
||||
|
||||
credManager := cred.GetCredManager(organization.PasswordType)
|
||||
passwordType := user.PasswordType
|
||||
if passwordType == "" {
|
||||
passwordType = organization.PasswordType
|
||||
}
|
||||
credManager := cred.GetCredManager(passwordType)
|
||||
if credManager != nil {
|
||||
if organization.MasterPassword != "" {
|
||||
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
|
||||
@ -207,7 +211,7 @@ func checkLdapUserPassword(user *User, password string, lang string) string {
|
||||
}
|
||||
|
||||
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
|
||||
0, 0, false, ldapServer.buildFilterString(user), []string{}, nil)
|
||||
0, 0, false, ldapServer.buildAuthFilterString(user), []string{}, nil)
|
||||
|
||||
searchResult, err := conn.Conn.Search(searchReq)
|
||||
if err != nil {
|
||||
@ -317,6 +321,10 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
|
||||
}
|
||||
|
||||
func CheckAccessPermission(userId string, application *Application) (bool, error) {
|
||||
if userId == "built-in/admin" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
permissions := GetPermissions(application.Organization)
|
||||
allowed := true
|
||||
var err error
|
||||
|
@ -90,7 +90,7 @@ func initBuiltInOrganization() bool {
|
||||
CountryCodes: []string{"US", "ES", "CN", "FR", "DE", "GB", "JP", "KR", "VN", "ID", "SG", "IN"},
|
||||
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
|
||||
Tags: []string{},
|
||||
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi"},
|
||||
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"},
|
||||
InitScore: 2000,
|
||||
AccountItems: getBuiltInAccountItems(),
|
||||
EnableSoftDeletion: false,
|
||||
|
@ -87,11 +87,13 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) {
|
||||
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
|
||||
continue
|
||||
}
|
||||
existed, failed := SyncLdapUsers(ldap.Owner, LdapUsersToLdapRespUsers(users), ldap.Id)
|
||||
if len(*failed) != 0 {
|
||||
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(*existed)-len(*failed), len(*failed)), *failed)
|
||||
|
||||
existed, failed, err := SyncLdapUsers(ldap.Owner, AutoAdjustLdapUser(users), ldap.Id)
|
||||
if len(failed) != 0 {
|
||||
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
|
||||
logs.Warning(err.Error())
|
||||
} else {
|
||||
logs.Info(fmt.Sprintf("ldap autosync success, %d new users, %d existing users", len(users)-len(*existed), len(*existed)))
|
||||
logs.Info(fmt.Sprintf("ldap autosync success, %d new users, %d existing users", len(users)-len(existed), len(existed)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/thanhpk/randstr"
|
||||
@ -35,35 +34,26 @@ type LdapConn struct {
|
||||
// Cn string
|
||||
//}
|
||||
|
||||
type ldapUser struct {
|
||||
UidNumber string
|
||||
Uid string
|
||||
Cn string
|
||||
GidNumber string
|
||||
type LdapUser struct {
|
||||
UidNumber string `json:"uidNumber"`
|
||||
Uid string `json:"uid"`
|
||||
Cn string `json:"cn"`
|
||||
GidNumber string `json:"gidNumber"`
|
||||
// Gcn string
|
||||
Uuid string
|
||||
DisplayName string
|
||||
Uuid string `json:"uuid"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Mail string
|
||||
Email string
|
||||
Email string `json:"email"`
|
||||
EmailAddress string
|
||||
TelephoneNumber string
|
||||
Mobile string
|
||||
MobileTelephoneNumber string
|
||||
RegisteredAddress string
|
||||
PostalAddress string
|
||||
}
|
||||
|
||||
type LdapRespUser struct {
|
||||
UidNumber string `json:"uidNumber"`
|
||||
Uid string `json:"uid"`
|
||||
Cn string `json:"cn"`
|
||||
GroupId string `json:"groupId"`
|
||||
// GroupName string `json:"groupName"`
|
||||
Uuid string `json:"uuid"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
GroupId string `json:"groupId"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
||||
@ -136,7 +126,7 @@ func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
|
||||
return isMicrosoft, err
|
||||
}
|
||||
|
||||
func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]ldapUser, error) {
|
||||
func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
|
||||
SearchAttributes := []string{
|
||||
"uidNumber", "cn", "sn", "gidNumber", "entryUUID", "displayName", "mail", "email",
|
||||
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
|
||||
@ -159,9 +149,9 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]ldapUser, error) {
|
||||
return nil, errors.New("no result")
|
||||
}
|
||||
|
||||
var ldapUsers []ldapUser
|
||||
var ldapUsers []LdapUser
|
||||
for _, entry := range searchResult.Entries {
|
||||
var user ldapUser
|
||||
var user LdapUser
|
||||
for _, attribute := range entry.Attributes {
|
||||
switch attribute.Name {
|
||||
case "uidNumber":
|
||||
@ -241,35 +231,30 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]ldapUser, error) {
|
||||
// return groupMap, nil
|
||||
// }
|
||||
|
||||
func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
|
||||
res := make([]LdapRespUser, 0)
|
||||
for _, user := range users {
|
||||
res = append(res, LdapRespUser{
|
||||
UidNumber: user.UidNumber,
|
||||
Uid: user.Uid,
|
||||
Cn: user.Cn,
|
||||
GroupId: user.GidNumber,
|
||||
Uuid: user.Uuid,
|
||||
DisplayName: user.DisplayName,
|
||||
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
|
||||
Phone: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
|
||||
Address: util.ReturnAnyNotEmpty(user.PostalAddress, user.RegisteredAddress),
|
||||
})
|
||||
func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
|
||||
res := make([]LdapUser, len(users))
|
||||
for i, user := range users {
|
||||
res[i] = LdapUser{
|
||||
UidNumber: user.UidNumber,
|
||||
Uid: user.Uid,
|
||||
Cn: user.Cn,
|
||||
GroupId: user.GidNumber,
|
||||
Uuid: user.GetLdapUuid(),
|
||||
DisplayName: user.DisplayName,
|
||||
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
|
||||
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
|
||||
RegisteredAddress: util.ReturnAnyNotEmpty(user.PostalAddress, user.RegisteredAddress),
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func SyncLdapUsers(owner string, respUsers []LdapRespUser, ldapId string) (*[]LdapRespUser, *[]LdapRespUser) {
|
||||
var existUsers []LdapRespUser
|
||||
var failedUsers []LdapRespUser
|
||||
func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUsers []LdapUser, failedUsers []LdapUser, err error) {
|
||||
var uuids []string
|
||||
|
||||
for _, user := range respUsers {
|
||||
for _, user := range syncUsers {
|
||||
uuids = append(uuids, user.Uuid)
|
||||
}
|
||||
|
||||
existUuids := GetExistUuids(owner, uuids)
|
||||
|
||||
organization := getOrganization("admin", owner)
|
||||
ldap := GetLdap(ldapId)
|
||||
|
||||
@ -289,67 +274,59 @@ func SyncLdapUsers(owner string, respUsers []LdapRespUser, ldapId string) (*[]Ld
|
||||
}
|
||||
tag := strings.Join(ou, ".")
|
||||
|
||||
for _, respUser := range respUsers {
|
||||
for _, syncUser := range syncUsers {
|
||||
existUuids := GetExistUuids(owner, uuids)
|
||||
found := false
|
||||
if len(existUuids) > 0 {
|
||||
for _, existUuid := range existUuids {
|
||||
if respUser.Uuid == existUuid {
|
||||
existUsers = append(existUsers, respUser)
|
||||
if syncUser.Uuid == existUuid {
|
||||
existUsers = append(existUsers, syncUser)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
score, _ := organization.GetInitScore()
|
||||
newUser := &User{
|
||||
Owner: owner,
|
||||
Name: respUser.buildLdapUserName(),
|
||||
Name: syncUser.buildLdapUserName(),
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: respUser.buildLdapDisplayName(),
|
||||
DisplayName: syncUser.buildLdapDisplayName(),
|
||||
Avatar: organization.DefaultAvatar,
|
||||
Email: respUser.Email,
|
||||
Phone: respUser.Phone,
|
||||
Address: []string{respUser.Address},
|
||||
Email: syncUser.Email,
|
||||
Phone: syncUser.Phone,
|
||||
Address: []string{syncUser.Address},
|
||||
Affiliation: affiliation,
|
||||
Tag: tag,
|
||||
Score: beego.AppConfig.DefaultInt("initScore", 2000),
|
||||
Ldap: respUser.Uuid,
|
||||
Score: score,
|
||||
Ldap: syncUser.Uuid,
|
||||
}
|
||||
|
||||
affected := AddUser(newUser)
|
||||
if !affected {
|
||||
failedUsers = append(failedUsers, respUser)
|
||||
failedUsers = append(failedUsers, syncUser)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &existUsers, &failedUsers
|
||||
return existUsers, failedUsers, err
|
||||
}
|
||||
|
||||
func GetExistUuids(owner string, uuids []string) []string {
|
||||
var users []User
|
||||
var existUuids []string
|
||||
existUuidSet := make(map[string]struct{})
|
||||
|
||||
err := adapter.Engine.Where(fmt.Sprintf("ldap IN (%s) AND owner = ?", "'"+strings.Join(uuids, "','")+"'"), owner).Find(&users)
|
||||
err := adapter.Engine.Table("user").Where("owner = ?", owner).Cols("ldap").
|
||||
In("ldap", uuids).Select("DISTINCT ldap").Find(&existUuids)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
for _, result := range users {
|
||||
existUuidSet[result.Ldap] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for uuid := range existUuidSet {
|
||||
existUuids = append(existUuids, uuid)
|
||||
}
|
||||
return existUuids
|
||||
}
|
||||
|
||||
func (ldapUser *LdapRespUser) buildLdapUserName() string {
|
||||
func (ldapUser *LdapUser) buildLdapUserName() string {
|
||||
user := User{}
|
||||
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
|
||||
has, err := adapter.Engine.Where("name = ? or name = ?", ldapUser.Uid, uidWithNumber).Get(&user)
|
||||
@ -364,10 +341,14 @@ func (ldapUser *LdapRespUser) buildLdapUserName() string {
|
||||
return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6))
|
||||
}
|
||||
|
||||
return ldapUser.Uid
|
||||
if ldapUser.Uid != "" {
|
||||
return ldapUser.Uid
|
||||
}
|
||||
|
||||
return ldapUser.Cn
|
||||
}
|
||||
|
||||
func (ldapUser *LdapRespUser) buildLdapDisplayName() string {
|
||||
func (ldapUser *LdapUser) buildLdapDisplayName() string {
|
||||
if ldapUser.DisplayName != "" {
|
||||
return ldapUser.DisplayName
|
||||
}
|
||||
@ -375,7 +356,18 @@ func (ldapUser *LdapRespUser) buildLdapDisplayName() string {
|
||||
return ldapUser.Cn
|
||||
}
|
||||
|
||||
func (ldap *Ldap) buildFilterString(user *User) string {
|
||||
func (ldapUser *LdapUser) GetLdapUuid() string {
|
||||
if ldapUser.Uuid != "" {
|
||||
return ldapUser.Uuid
|
||||
}
|
||||
if ldapUser.Uid != "" {
|
||||
return ldapUser.Uid
|
||||
}
|
||||
|
||||
return ldapUser.Cn
|
||||
}
|
||||
|
||||
func (ldap *Ldap) buildAuthFilterString(user *User) string {
|
||||
if len(ldap.FilterFields) == 0 {
|
||||
return fmt.Sprintf("(&%s(uid=%s))", ldap.Filter, user.Name)
|
||||
}
|
||||
@ -393,6 +385,8 @@ func (user *User) getFieldFromLdapAttribute(attribute string) string {
|
||||
switch attribute {
|
||||
case "uid":
|
||||
return user.Name
|
||||
case "sAMAccountName":
|
||||
return user.Name
|
||||
case "mail":
|
||||
return user.Email
|
||||
case "mobile":
|
||||
|
@ -51,6 +51,7 @@ const (
|
||||
const (
|
||||
MfaSessionUserId = "MfaSessionUserId"
|
||||
NextMfa = "NextMfa"
|
||||
RequiredMfa = "RequiredMfa"
|
||||
)
|
||||
|
||||
func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
|
||||
|
@ -26,6 +26,7 @@ func DoMigration() {
|
||||
&Migrator_1_101_0_PR_1083{},
|
||||
&Migrator_1_235_0_PR_1530{},
|
||||
&Migrator_1_240_0_PR_1539{},
|
||||
&Migrator_1_314_0_PR_1841{},
|
||||
// more migrators add here in chronological order...
|
||||
}
|
||||
|
||||
|
75
object/migrator_1_314_0_PR_1841.go
Normal file
75
object/migrator_1_314_0_PR_1841.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"github.com/xorm-io/xorm"
|
||||
"github.com/xorm-io/xorm/migrate"
|
||||
)
|
||||
|
||||
type Migrator_1_314_0_PR_1841 struct{}
|
||||
|
||||
func (*Migrator_1_314_0_PR_1841) IsMigrationNeeded() bool {
|
||||
users := []*User{}
|
||||
|
||||
err := adapter.Engine.Table("user").Find(&users)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if u.PasswordType != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (*Migrator_1_314_0_PR_1841) DoMigration() *migrate.Migration {
|
||||
migration := migrate.Migration{
|
||||
ID: "20230515MigrateUser--Create a new field 'passwordType' for table `user`",
|
||||
Migrate: func(engine *xorm.Engine) error {
|
||||
tx := engine.NewSession()
|
||||
|
||||
defer tx.Close()
|
||||
|
||||
err := tx.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
organizations := []*Organization{}
|
||||
err = tx.Table("organization").Find(&organizations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, organization := range organizations {
|
||||
user := &User{PasswordType: organization.PasswordType}
|
||||
_, err = tx.Where("owner = ?", organization.Name).Cols("password_type").Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return &migration
|
||||
}
|
@ -16,7 +16,9 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/cred"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -38,6 +40,11 @@ type ThemeData struct {
|
||||
IsEnabled bool `xorm:"bool" json:"isEnabled"`
|
||||
}
|
||||
|
||||
type MfaItem struct {
|
||||
Name string `json:"name"`
|
||||
Rule string `json:"rule"`
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
@ -59,6 +66,7 @@ type Organization struct {
|
||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||
IsProfilePublic bool `json:"isProfilePublic"`
|
||||
|
||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||
AccountItems []*AccountItem `xorm:"varchar(3000)" json:"accountItems"`
|
||||
}
|
||||
|
||||
@ -408,3 +416,20 @@ func organizationChangeTrigger(oldName string, newName string) error {
|
||||
|
||||
return session.Commit()
|
||||
}
|
||||
|
||||
func (org *Organization) HasRequiredMfa() bool {
|
||||
for _, item := range org.MfaItems {
|
||||
if item.Rule == "Required" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (org *Organization) GetInitScore() (int, error) {
|
||||
if org != nil {
|
||||
return org.InitScore, nil
|
||||
} else {
|
||||
return strconv.Atoi(conf.GetConfigString("initScore"))
|
||||
}
|
||||
}
|
||||
|
@ -235,6 +235,16 @@ func GetPermissionsByRole(roleId string) []*Permission {
|
||||
return permissions
|
||||
}
|
||||
|
||||
func GetPermissionsByResource(resourceId string) []*Permission {
|
||||
permissions := []*Permission{}
|
||||
err := adapter.Engine.Where("resources like ?", "%"+resourceId+"\"%").Find(&permissions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return permissions
|
||||
}
|
||||
|
||||
func GetPermissionsBySubmitter(owner string, submitter string) []*Permission {
|
||||
permissions := []*Permission{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&permissions, &Permission{Owner: owner, Submitter: submitter})
|
||||
|
145
object/plan.go
Normal file
145
object/plan.go
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright 2023 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"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Plan 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"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
|
||||
PricePerMonth float64 `json:"pricePerMonth"`
|
||||
PricePerYear float64 `json:"pricePerYear"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
|
||||
Role string `xorm:"varchar(100)" json:"role"`
|
||||
Options []string `xorm:"-" json:"options"`
|
||||
}
|
||||
|
||||
func GetPlanCount(owner, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Plan{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return int(count)
|
||||
}
|
||||
|
||||
func GetPlans(owner string) []*Plan {
|
||||
plans := []*Plan{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&plans, &Plan{Owner: owner})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return plans
|
||||
}
|
||||
|
||||
func GetPaginatedPlans(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Plan {
|
||||
plans := []*Plan{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&plans)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return plans
|
||||
}
|
||||
|
||||
func getPlan(owner, name string) *Plan {
|
||||
if owner == "" || name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
plan := Plan{Owner: owner, Name: name}
|
||||
existed, err := adapter.Engine.Get(&plan)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if existed {
|
||||
return &plan
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetPlan(id string) *Plan {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getPlan(owner, name)
|
||||
}
|
||||
|
||||
func UpdatePlan(id string, plan *Plan) bool {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if getPlan(owner, name) == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(plan)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func AddPlan(plan *Plan) bool {
|
||||
affected, err := adapter.Engine.Insert(plan)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func DeletePlan(plan *Plan) bool {
|
||||
affected, err := adapter.Engine.ID(core.PK{plan.Owner, plan.Name}).Delete(plan)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func (plan *Plan) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
|
||||
}
|
||||
|
||||
func Subscribe(owner string, user string, plan string, pricing string) *Subscription {
|
||||
selectedPricing := GetPricing(fmt.Sprintf("%s/%s", owner, pricing))
|
||||
|
||||
valid := selectedPricing != nil && selectedPricing.IsEnabled
|
||||
|
||||
if !valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
planBelongToPricing := selectedPricing.HasPlan(owner, plan)
|
||||
|
||||
if planBelongToPricing {
|
||||
newSubscription := NewSubscription(owner, user, plan, selectedPricing.TrialDuration)
|
||||
affected := AddSubscription(newSubscription)
|
||||
|
||||
if affected {
|
||||
return newSubscription
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
147
object/pricing.go
Normal file
147
object/pricing.go
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Pricing 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"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
|
||||
Plans []string `xorm:"mediumtext" json:"plans"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
HasTrial bool `json:"hasTrial"`
|
||||
TrialDuration int `json:"trialDuration"`
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
|
||||
Submitter string `xorm:"varchar(100)" json:"submitter"`
|
||||
Approver string `xorm:"varchar(100)" json:"approver"`
|
||||
ApproveTime string `xorm:"varchar(100)" json:"approveTime"`
|
||||
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
}
|
||||
|
||||
func GetPricingCount(owner, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Pricing{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return int(count)
|
||||
}
|
||||
|
||||
func GetPricings(owner string) []*Pricing {
|
||||
pricings := []*Pricing{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&pricings, &Pricing{Owner: owner})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pricings
|
||||
}
|
||||
|
||||
func GetPaginatedPricings(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Pricing {
|
||||
pricings := []*Pricing{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&pricings)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pricings
|
||||
}
|
||||
|
||||
func getPricing(owner, name string) *Pricing {
|
||||
if owner == "" || name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
pricing := Pricing{Owner: owner, Name: name}
|
||||
existed, err := adapter.Engine.Get(&pricing)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if existed {
|
||||
return &pricing
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetPricing(id string) *Pricing {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getPricing(owner, name)
|
||||
}
|
||||
|
||||
func UpdatePricing(id string, pricing *Pricing) bool {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if getPricing(owner, name) == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(pricing)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func AddPricing(pricing *Pricing) bool {
|
||||
affected, err := adapter.Engine.Insert(pricing)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func DeletePricing(pricing *Pricing) bool {
|
||||
affected, err := adapter.Engine.ID(core.PK{pricing.Owner, pricing.Name}).Delete(pricing)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func (pricing *Pricing) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", pricing.Owner, pricing.Name)
|
||||
}
|
||||
|
||||
func (pricing *Pricing) HasPlan(owner string, plan string) bool {
|
||||
selectedPlan := GetPlan(fmt.Sprintf("%s/%s", owner, plan))
|
||||
|
||||
if selectedPlan == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
result := false
|
||||
|
||||
for _, pricingPlan := range pricing.Plans {
|
||||
if strings.Contains(pricingPlan, selectedPlan.Name) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -70,7 +70,11 @@ type Provider struct {
|
||||
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
|
||||
}
|
||||
|
||||
func GetMaskedProvider(provider *Provider) *Provider {
|
||||
func GetMaskedProvider(provider *Provider, isMaskEnabled bool) *Provider {
|
||||
if !isMaskEnabled {
|
||||
return provider
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
return nil
|
||||
}
|
||||
@ -88,9 +92,13 @@ func GetMaskedProvider(provider *Provider) *Provider {
|
||||
return provider
|
||||
}
|
||||
|
||||
func GetMaskedProviders(providers []*Provider) []*Provider {
|
||||
func GetMaskedProviders(providers []*Provider, isMaskEnabled bool) []*Provider {
|
||||
if !isMaskEnabled {
|
||||
return providers
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
provider = GetMaskedProvider(provider)
|
||||
provider = GetMaskedProvider(provider, isMaskEnabled)
|
||||
}
|
||||
return providers
|
||||
}
|
||||
@ -310,7 +318,7 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
|
||||
continue
|
||||
}
|
||||
if provider.Provider.Category == "Captcha" {
|
||||
return GetCaptchaProviderByOwnerName(fmt.Sprintf("%s/%s", provider.Provider.Owner, provider.Provider.Name), lang)
|
||||
return GetCaptchaProviderByOwnerName(util.GetId(provider.Provider.Owner, provider.Provider.Name), lang)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -47,7 +47,8 @@ type Record struct {
|
||||
RequestUri string `xorm:"varchar(1000)" json:"requestUri"`
|
||||
Action string `xorm:"varchar(1000)" json:"action"`
|
||||
|
||||
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
||||
Object string `xorm:"-" json:"object"`
|
||||
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
||||
|
||||
IsTriggered bool `json:"isTriggered"`
|
||||
}
|
||||
@ -60,6 +61,11 @@ func NewRecord(ctx *context.Context) *Record {
|
||||
requestUri = requestUri[0:1000]
|
||||
}
|
||||
|
||||
object := ""
|
||||
if ctx.Input.RequestBody != nil && len(ctx.Input.RequestBody) != 0 {
|
||||
object = string(ctx.Input.RequestBody)
|
||||
}
|
||||
|
||||
record := Record{
|
||||
Name: util.GenerateId(),
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
@ -68,6 +74,7 @@ func NewRecord(ctx *context.Context) *Record {
|
||||
Method: ctx.Request.Method,
|
||||
RequestUri: requestUri,
|
||||
Action: action,
|
||||
Object: object,
|
||||
IsTriggered: false,
|
||||
}
|
||||
return &record
|
||||
@ -159,7 +166,7 @@ func SendWebhooks(record *Record) error {
|
||||
|
||||
if matched {
|
||||
if webhook.IsUserExtended {
|
||||
user := getUser(record.Organization, record.User)
|
||||
user := GetMaskedUser(getUser(record.Organization, record.User))
|
||||
record.ExtendedUser = user
|
||||
}
|
||||
|
||||
|
154
object/subscription.go
Normal file
154
object/subscription.go
Normal file
@ -0,0 +1,154 @@
|
||||
// Copyright 2023 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"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
const defaultStatus = "Pending"
|
||||
|
||||
type Subscription 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"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Duration int `json:"duration"`
|
||||
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
Plan string `xorm:"varchar(100)" json:"plan"`
|
||||
|
||||
StartDate time.Time `json:"startDate"`
|
||||
EndDate time.Time `json:"endDate"`
|
||||
|
||||
User string `xorm:"mediumtext" json:"user"`
|
||||
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
Submitter string `xorm:"varchar(100)" json:"submitter"`
|
||||
Approver string `xorm:"varchar(100)" json:"approver"`
|
||||
ApproveTime string `xorm:"varchar(100)" json:"approveTime"`
|
||||
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
}
|
||||
|
||||
func NewSubscription(owner string, user string, plan string, duration int) *Subscription {
|
||||
id := util.GenerateId()[:6]
|
||||
return &Subscription{
|
||||
Name: "Subscription_" + id,
|
||||
DisplayName: "New Subscription - " + id,
|
||||
Owner: owner,
|
||||
User: owner + "/" + user,
|
||||
Plan: owner + "/" + plan,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
State: defaultStatus,
|
||||
Duration: duration,
|
||||
StartDate: time.Now(),
|
||||
EndDate: time.Now().AddDate(0, 0, duration),
|
||||
}
|
||||
}
|
||||
|
||||
func GetSubscriptionCount(owner, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Subscription{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return int(count)
|
||||
}
|
||||
|
||||
func GetSubscriptions(owner string) []*Subscription {
|
||||
subscriptions := []*Subscription{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&subscriptions, &Subscription{Owner: owner})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return subscriptions
|
||||
}
|
||||
|
||||
func GetPaginationSubscriptions(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Subscription {
|
||||
subscriptions := []*Subscription{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&subscriptions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return subscriptions
|
||||
}
|
||||
|
||||
func getSubscription(owner string, name string) *Subscription {
|
||||
if owner == "" || name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
subscription := Subscription{Owner: owner, Name: name}
|
||||
existed, err := adapter.Engine.Get(&subscription)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &subscription
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetSubscription(id string) *Subscription {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getSubscription(owner, name)
|
||||
}
|
||||
|
||||
func UpdateSubscription(id string, subscription *Subscription) bool {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if getSubscription(owner, name) == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(subscription)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func AddSubscription(subscription *Subscription) bool {
|
||||
affected, err := adapter.Engine.Insert(subscription)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func DeleteSubscription(subscription *Subscription) bool {
|
||||
affected, err := adapter.Engine.ID(core.PK{subscription.Owner, subscription.Name}).Delete(&Subscription{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func (subscription *Subscription) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", subscription.Owner, subscription.Name)
|
||||
}
|
@ -224,13 +224,16 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
||||
nowTime := time.Now()
|
||||
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
|
||||
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
|
||||
if application.RefreshExpireInHours == 0 {
|
||||
refreshExpireTime = expireTime
|
||||
}
|
||||
|
||||
user = refineUser(user)
|
||||
|
||||
_, originBackend := getOriginFromHost(host)
|
||||
|
||||
name := util.GenerateId()
|
||||
jti := fmt.Sprintf("%s/%s", application.Owner, name)
|
||||
jti := util.GetId(application.Owner, name)
|
||||
|
||||
claims := Claims{
|
||||
User: user,
|
||||
|
@ -41,6 +41,7 @@ type User struct {
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
Password string `xorm:"varchar(100)" json:"password"`
|
||||
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
FirstName string `xorm:"varchar(100)" json:"firstName"`
|
||||
LastName string `xorm:"varchar(100)" json:"lastName"`
|
||||
@ -471,6 +472,13 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) bool {
|
||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
|
||||
"signin_wrong_times", "last_signin_wrong_time",
|
||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
|
||||
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
|
||||
"spotify", "strava", "stripe", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
|
||||
"yammer", "yandex", "zoom", "custom",
|
||||
}
|
||||
}
|
||||
if isAdmin {
|
||||
|
@ -35,5 +35,6 @@ func (user *User) UpdateUserPassword(organization *Organization) {
|
||||
if credManager != nil {
|
||||
hashedPassword := credManager.GetHashedPassword(user.Password, user.PasswordSalt, organization.PasswordSalt)
|
||||
user.Password = hashedPassword
|
||||
user.PasswordType = organization.PasswordType
|
||||
}
|
||||
}
|
||||
|
@ -77,13 +77,17 @@ func GetUserByFields(organization string, field string) *User {
|
||||
}
|
||||
|
||||
func SetUserField(user *User, field string, value string) bool {
|
||||
bean := make(map[string]interface{})
|
||||
if field == "password" {
|
||||
organization := GetOrganizationByUser(user)
|
||||
user.UpdateUserPassword(organization)
|
||||
value = user.Password
|
||||
bean[strings.ToLower(field)] = user.Password
|
||||
bean["password_type"] = user.PasswordType
|
||||
} else {
|
||||
bean[strings.ToLower(field)] = value
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{strings.ToLower(field): value})
|
||||
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(bean)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -125,7 +125,11 @@ func AuthzFilter(ctx *context.Context) {
|
||||
subOwner, subName := getSubject(ctx)
|
||||
method := ctx.Request.Method
|
||||
urlPath := getUrlPath(ctx.Request.URL.Path)
|
||||
objOwner, objName := getObject(ctx)
|
||||
|
||||
objOwner, objName := "", ""
|
||||
if urlPath != "/api/get-app-login" {
|
||||
objOwner, objName = getObject(ctx)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(urlPath, "/api/notify-payment") {
|
||||
urlPath = "/api/notify-payment"
|
||||
|
@ -43,7 +43,7 @@ func AutoSigninFilter(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", token.Organization, token.User)
|
||||
userId := util.GetId(token.Organization, token.User)
|
||||
application, _ := object.GetApplicationByUserId(fmt.Sprintf("app/%s", token.Application))
|
||||
setSessionUser(ctx, userId)
|
||||
setSessionOidc(ctx, token.Scope, application.ClientId)
|
||||
|
@ -15,8 +15,6 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/context"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -50,7 +48,7 @@ func getUserByClientIdSecret(ctx *context.Context) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s", application.Organization, application.Name)
|
||||
return util.GetId(application.Organization, application.Name)
|
||||
}
|
||||
|
||||
func RecordMessage(ctx *context.Context) {
|
||||
|
@ -203,6 +203,24 @@ func initAPI() {
|
||||
beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage")
|
||||
beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage")
|
||||
|
||||
beego.Router("/api/get-subscriptions", &controllers.ApiController{}, "GET:GetSubscriptions")
|
||||
beego.Router("/api/get-subscription", &controllers.ApiController{}, "GET:GetSubscription")
|
||||
beego.Router("/api/update-subscription", &controllers.ApiController{}, "POST:UpdateSubscription")
|
||||
beego.Router("/api/add-subscription", &controllers.ApiController{}, "POST:AddSubscription")
|
||||
beego.Router("/api/delete-subscription", &controllers.ApiController{}, "POST:DeleteSubscription")
|
||||
|
||||
beego.Router("/api/get-plans", &controllers.ApiController{}, "GET:GetPlans")
|
||||
beego.Router("/api/get-plan", &controllers.ApiController{}, "GET:GetPlan")
|
||||
beego.Router("/api/update-plan", &controllers.ApiController{}, "POST:UpdatePlan")
|
||||
beego.Router("/api/add-plan", &controllers.ApiController{}, "POST:AddPlan")
|
||||
beego.Router("/api/delete-plan", &controllers.ApiController{}, "POST:DeletePlan")
|
||||
|
||||
beego.Router("/api/get-pricings", &controllers.ApiController{}, "GET:GetPricings")
|
||||
beego.Router("/api/get-pricing", &controllers.ApiController{}, "GET:GetPricing")
|
||||
beego.Router("/api/update-pricing", &controllers.ApiController{}, "POST:UpdatePricing")
|
||||
beego.Router("/api/add-pricing", &controllers.ApiController{}, "POST:AddPricing")
|
||||
beego.Router("/api/delete-pricing", &controllers.ApiController{}, "POST:DeletePricing")
|
||||
|
||||
beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts")
|
||||
beego.Router("/api/get-product", &controllers.ApiController{}, "GET:GetProduct")
|
||||
beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")
|
||||
@ -247,6 +265,7 @@ func initAPI() {
|
||||
|
||||
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
|
||||
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo")
|
||||
beego.Router("/api/health", &controllers.ApiController{}, "GET:Health")
|
||||
beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")
|
||||
|
||||
beego.Handler("/api/metrics", promhttp.Handler())
|
||||
|
@ -15,6 +15,8 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@ -28,6 +30,7 @@ import (
|
||||
var (
|
||||
oldStaticBaseUrl = "https://cdn.casbin.org"
|
||||
newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
|
||||
enableGzip, _ = conf.GetConfigBool("enableGzip")
|
||||
)
|
||||
|
||||
func StaticFilter(ctx *context.Context) {
|
||||
@ -53,7 +56,7 @@ func StaticFilter(ctx *context.Context) {
|
||||
|
||||
path2 := strings.TrimLeft(path, "web/build/images/")
|
||||
if util.FileExist(path2) {
|
||||
http.ServeFile(ctx.ResponseWriter, ctx.Request, path2)
|
||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path2)
|
||||
return
|
||||
}
|
||||
|
||||
@ -62,7 +65,7 @@ func StaticFilter(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if oldStaticBaseUrl == newStaticBaseUrl {
|
||||
http.ServeFile(ctx.ResponseWriter, ctx.Request, path)
|
||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path)
|
||||
} else {
|
||||
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, oldStaticBaseUrl, newStaticBaseUrl)
|
||||
}
|
||||
@ -89,3 +92,24 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, o
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type gzipResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string) {
|
||||
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
http.ServeFile(w, r, path)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(w)
|
||||
defer gz.Close()
|
||||
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||
http.ServeFile(gzw, r, path)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -177,6 +177,42 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-plan:
|
||||
post:
|
||||
tags:
|
||||
- Plan API
|
||||
description: add plan
|
||||
operationId: ApiController.AddPlan
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the plan
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Plan'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-pricing:
|
||||
post:
|
||||
tags:
|
||||
- Pricing API
|
||||
description: add pricing
|
||||
operationId: ApiController.AddPricing
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the pricing
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Pricing'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-product:
|
||||
post:
|
||||
tags:
|
||||
@ -278,6 +314,24 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
/api/add-subscription:
|
||||
post:
|
||||
tags:
|
||||
- Subscription API
|
||||
description: add subscription
|
||||
operationId: ApiController.AddSubscription
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the subscription
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Subscription'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-syncer:
|
||||
post:
|
||||
tags:
|
||||
@ -552,6 +606,17 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-mfa/:
|
||||
post:
|
||||
tags:
|
||||
- MFA API
|
||||
description: ': Delete MFA'
|
||||
operationId: ApiController.DeleteMfa
|
||||
responses:
|
||||
"200":
|
||||
description: object
|
||||
schema:
|
||||
$ref: '#/definitions/Response'
|
||||
/api/delete-model:
|
||||
post:
|
||||
tags:
|
||||
@ -624,6 +689,42 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-plan:
|
||||
post:
|
||||
tags:
|
||||
- Plan API
|
||||
description: delete plan
|
||||
operationId: ApiController.DeletePlan
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the plan
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Plan'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-pricing:
|
||||
post:
|
||||
tags:
|
||||
- Pricing API
|
||||
description: delete pricing
|
||||
operationId: ApiController.DeletePricing
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the pricing
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Pricing'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-product:
|
||||
post:
|
||||
tags:
|
||||
@ -702,6 +803,24 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
/api/delete-subscription:
|
||||
post:
|
||||
tags:
|
||||
- Subscription API
|
||||
description: delete subscription
|
||||
operationId: ApiController.DeleteSubscription
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the subscription
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Subscription'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-syncer:
|
||||
post:
|
||||
tags:
|
||||
@ -995,6 +1114,19 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.User'
|
||||
/api/get-globle-certs:
|
||||
get:
|
||||
tags:
|
||||
- Cert API
|
||||
description: get globle certs
|
||||
operationId: ApiController.GetGlobleCerts
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Cert'
|
||||
/api/get-ldap:
|
||||
get:
|
||||
tags:
|
||||
@ -1027,6 +1159,23 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Message'
|
||||
/api/get-message-answer:
|
||||
get:
|
||||
tags:
|
||||
- Message API
|
||||
description: get message answer
|
||||
operationId: ApiController.GetMessageAnswer
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the message
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Message'
|
||||
/api/get-messages:
|
||||
get:
|
||||
tags:
|
||||
@ -1241,6 +1390,82 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Permission'
|
||||
/api/get-plan:
|
||||
get:
|
||||
tags:
|
||||
- Plan API
|
||||
description: get plan
|
||||
operationId: ApiController.GetPlan
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the plan
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: includeOption
|
||||
description: Should include plan's option
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Plan'
|
||||
/api/get-plans:
|
||||
get:
|
||||
tags:
|
||||
- Plan API
|
||||
description: get plans
|
||||
operationId: ApiController.GetPlans
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of plans
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Plan'
|
||||
/api/get-pricing:
|
||||
get:
|
||||
tags:
|
||||
- Pricing API
|
||||
description: get pricing
|
||||
operationId: ApiController.GetPricing
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the pricing
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.pricing'
|
||||
/api/get-pricings:
|
||||
get:
|
||||
tags:
|
||||
- Pricing API
|
||||
description: get pricings
|
||||
operationId: ApiController.GetPricings
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of pricings
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Pricing'
|
||||
/api/get-product:
|
||||
get:
|
||||
tags:
|
||||
@ -1277,6 +1502,17 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Product'
|
||||
/api/get-prometheus-info:
|
||||
get:
|
||||
tags:
|
||||
- Prometheus API
|
||||
description: get Prometheus Info
|
||||
operationId: ApiController.GetPrometheusInfo
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.PrometheusInfo'
|
||||
/api/get-provider:
|
||||
get:
|
||||
tags:
|
||||
@ -1466,6 +1702,42 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.User'
|
||||
/api/get-subscription:
|
||||
get:
|
||||
tags:
|
||||
- Subscription API
|
||||
description: get subscription
|
||||
operationId: ApiController.GetSubscription
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the subscription
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.subscription'
|
||||
/api/get-subscriptions:
|
||||
get:
|
||||
tags:
|
||||
- Subscription API
|
||||
description: get subscriptions
|
||||
operationId: ApiController.GetSubscriptions
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of subscriptions
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Subscription'
|
||||
/api/get-syncer:
|
||||
get:
|
||||
tags:
|
||||
@ -1975,6 +2247,39 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/mfa/setup/enable:
|
||||
post:
|
||||
tags:
|
||||
- MFA API
|
||||
description: enable totp
|
||||
operationId: ApiController.MfaSetupEnable
|
||||
responses:
|
||||
"200":
|
||||
description: object
|
||||
schema:
|
||||
$ref: '#/definitions/Response'
|
||||
/api/mfa/setup/initiate:
|
||||
post:
|
||||
tags:
|
||||
- MFA API
|
||||
description: setup MFA
|
||||
operationId: ApiController.MfaSetupInitiate
|
||||
responses:
|
||||
"200":
|
||||
description: Response object
|
||||
schema:
|
||||
$ref: '#/definitions/The'
|
||||
/api/mfa/setup/verify:
|
||||
post:
|
||||
tags:
|
||||
- MFA API
|
||||
description: setup verify totp
|
||||
operationId: ApiController.MfaSetupVerify
|
||||
responses:
|
||||
"200":
|
||||
description: object
|
||||
schema:
|
||||
$ref: '#/definitions/Response'
|
||||
/api/notify-payment:
|
||||
post:
|
||||
tags:
|
||||
@ -2048,6 +2353,17 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/set-preferred-mfa:
|
||||
post:
|
||||
tags:
|
||||
- MFA API
|
||||
description: ': Set specific Mfa Preferred'
|
||||
operationId: ApiController.SetPreferredMfa
|
||||
responses:
|
||||
"200":
|
||||
description: object
|
||||
schema:
|
||||
$ref: '#/definitions/Response'
|
||||
/api/signup:
|
||||
post:
|
||||
tags:
|
||||
@ -2268,6 +2584,52 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-plan:
|
||||
post:
|
||||
tags:
|
||||
- Plan API
|
||||
description: update plan
|
||||
operationId: ApiController.UpdatePlan
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the plan
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the plan
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Plan'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-pricing:
|
||||
post:
|
||||
tags:
|
||||
- Pricing API
|
||||
description: update pricing
|
||||
operationId: ApiController.UpdatePricing
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the pricing
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the pricing
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Pricing'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-product:
|
||||
post:
|
||||
tags:
|
||||
@ -2361,6 +2723,29 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
/api/update-subscription:
|
||||
post:
|
||||
tags:
|
||||
- Subscription API
|
||||
description: update subscription
|
||||
operationId: ApiController.UpdateSubscription
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the subscription
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the subscription
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Subscription'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-syncer:
|
||||
post:
|
||||
tags:
|
||||
@ -2555,10 +2940,10 @@ paths:
|
||||
schema:
|
||||
$ref: '#/definitions/Response'
|
||||
definitions:
|
||||
1183.0xc000455050.false:
|
||||
1183.0x1400042eb70.false:
|
||||
title: "false"
|
||||
type: object
|
||||
1217.0xc000455080.false:
|
||||
1217.0x1400042eba0.false:
|
||||
title: "false"
|
||||
type: object
|
||||
LaravelResponse:
|
||||
@ -2567,6 +2952,9 @@ definitions:
|
||||
Response:
|
||||
title: Response
|
||||
type: object
|
||||
The:
|
||||
title: The
|
||||
type: object
|
||||
controllers.AuthForm:
|
||||
title: AuthForm
|
||||
type: object
|
||||
@ -2591,9 +2979,9 @@ definitions:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/1183.0xc000455050.false'
|
||||
$ref: '#/definitions/1183.0x1400042eb70.false'
|
||||
data2:
|
||||
$ref: '#/definitions/1217.0xc000455080.false'
|
||||
$ref: '#/definitions/1217.0x1400042eba0.false'
|
||||
msg:
|
||||
type: string
|
||||
name:
|
||||
@ -2799,6 +3187,17 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
object.GaugeVecInfo:
|
||||
title: GaugeVecInfo
|
||||
type: object
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
throughput:
|
||||
type: number
|
||||
format: double
|
||||
object.Header:
|
||||
title: Header
|
||||
type: object
|
||||
@ -2807,6 +3206,19 @@ definitions:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
object.HistogramVecInfo:
|
||||
title: HistogramVecInfo
|
||||
type: object
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
format: int64
|
||||
latency:
|
||||
type: string
|
||||
method:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
object.IntrospectionResponse:
|
||||
title: IntrospectionResponse
|
||||
type: object
|
||||
@ -2868,8 +3280,30 @@ definitions:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
replyTo:
|
||||
type: string
|
||||
text:
|
||||
type: string
|
||||
object.MfaProps:
|
||||
title: MfaProps
|
||||
type: object
|
||||
properties:
|
||||
countryCode:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
isPreferred:
|
||||
type: boolean
|
||||
recoveryCodes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
secret:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
object.Model:
|
||||
title: Model
|
||||
type: object
|
||||
@ -3098,6 +3532,67 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
object.Plan:
|
||||
title: Plan
|
||||
type: object
|
||||
properties:
|
||||
createdTime:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
pricePerMonth:
|
||||
type: number
|
||||
format: double
|
||||
pricePerYear:
|
||||
type: number
|
||||
format: double
|
||||
role:
|
||||
type: string
|
||||
options:
|
||||
type: array
|
||||
object.Pricing:
|
||||
title: Pricing
|
||||
type: object
|
||||
properties:
|
||||
application:
|
||||
type: string
|
||||
approveTime:
|
||||
type: string
|
||||
approver:
|
||||
type: string
|
||||
createdTime:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
hasTrial:
|
||||
type: boolean
|
||||
isEnabled:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
plans:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
state:
|
||||
type: string
|
||||
submitter:
|
||||
type: string
|
||||
trialDuration:
|
||||
type: integer
|
||||
format: int64
|
||||
object.Product:
|
||||
title: Product
|
||||
type: object
|
||||
@ -3141,6 +3636,21 @@ definitions:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
object.PrometheusInfo:
|
||||
title: PrometheusInfo
|
||||
type: object
|
||||
properties:
|
||||
apiLatency:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.HistogramVecInfo'
|
||||
apiThroughput:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.GaugeVecInfo'
|
||||
totalThroughput:
|
||||
type: number
|
||||
format: double
|
||||
object.Provider:
|
||||
title: Provider
|
||||
type: object
|
||||
@ -3313,6 +3823,43 @@ definitions:
|
||||
type: string
|
||||
visible:
|
||||
type: boolean
|
||||
object.Subscription:
|
||||
title: Subscription
|
||||
type: object
|
||||
properties:
|
||||
approveTime:
|
||||
type: string
|
||||
approver:
|
||||
type: string
|
||||
createdTime:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
duration:
|
||||
type: integer
|
||||
format: int64
|
||||
endDate:
|
||||
type: string
|
||||
format: datetime
|
||||
isEnabled:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
plan:
|
||||
type: string
|
||||
startDate:
|
||||
type: string
|
||||
format: datetime
|
||||
state:
|
||||
type: string
|
||||
submitter:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
object.Syncer:
|
||||
title: Syncer
|
||||
type: object
|
||||
@ -3612,6 +4159,10 @@ definitions:
|
||||
type: string
|
||||
microsoftonline:
|
||||
type: string
|
||||
multiFactorAuths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.MfaProps'
|
||||
name:
|
||||
type: string
|
||||
naver:
|
||||
@ -3778,6 +4329,12 @@ definitions:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
object.pricing:
|
||||
title: pricing
|
||||
type: object
|
||||
object.subscription:
|
||||
title: subscription
|
||||
type: object
|
||||
protocol.CredentialAssertion:
|
||||
title: CredentialAssertion
|
||||
type: object
|
||||
|
@ -44,6 +44,12 @@ import SyncerListPage from "./SyncerListPage";
|
||||
import SyncerEditPage from "./SyncerEditPage";
|
||||
import CertListPage from "./CertListPage";
|
||||
import CertEditPage from "./CertEditPage";
|
||||
import SubscriptionListPage from "./SubscriptionListPage";
|
||||
import SubscriptionEditPage from "./SubscriptionEditPage";
|
||||
import PricingListPage from "./PricingListPage";
|
||||
import PricingEditPage from "./PricingEditPage";
|
||||
import PlanListPage from "./PlanListPage";
|
||||
import PlanEditPage from "./PlanEditPage";
|
||||
import ChatListPage from "./ChatListPage";
|
||||
import ChatEditPage from "./ChatEditPage";
|
||||
import ChatPage from "./ChatPage";
|
||||
@ -168,6 +174,12 @@ class App extends Component {
|
||||
this.setState({selectedMenuKey: "/result"});
|
||||
} else if (uri.includes("/sysinfo")) {
|
||||
this.setState({selectedMenuKey: "/sysinfo"});
|
||||
} else if (uri.includes("/subscriptions")) {
|
||||
this.setState({selectedMenuKey: "/subscriptions"});
|
||||
} else if (uri.includes("/plans")) {
|
||||
this.setState({selectedMenuKey: "/plans"});
|
||||
} else if (uri.includes("/pricings")) {
|
||||
this.setState({selectedMenuKey: "/pricings"});
|
||||
} else {
|
||||
this.setState({selectedMenuKey: -1});
|
||||
}
|
||||
@ -335,6 +347,8 @@ class App extends Component {
|
||||
const onClick = (e) => {
|
||||
if (e.key === "/account") {
|
||||
this.props.history.push("/account");
|
||||
} else if (e.key === "/subscription") {
|
||||
this.props.history.push("/subscription");
|
||||
} else if (e.key === "/chat") {
|
||||
this.props.history.push("/chat");
|
||||
} else if (e.key === "/logout") {
|
||||
@ -444,6 +458,19 @@ class App extends Component {
|
||||
res.push(Setting.getItem(<Link to="/records">{i18next.t("general:Records")}</Link>,
|
||||
"/records"
|
||||
));
|
||||
|
||||
res.push(Setting.getItem(<Link to="/plans">{i18next.t("general:Plans")}</Link>,
|
||||
"/plans"
|
||||
));
|
||||
|
||||
res.push(Setting.getItem(<Link to="/pricings">{i18next.t("general:Pricings")}</Link>,
|
||||
"/pricings"
|
||||
));
|
||||
|
||||
res.push(Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>,
|
||||
"/subscriptions"
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
if (Setting.isLocalAdminUser(this.state.account)) {
|
||||
@ -468,6 +495,7 @@ class App extends Component {
|
||||
));
|
||||
|
||||
if (Conf.EnableExtraPages) {
|
||||
|
||||
res.push(Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>,
|
||||
"/products"
|
||||
));
|
||||
@ -550,12 +578,18 @@ class App extends Component {
|
||||
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/certs/:organizationName/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/chats" render={(props) => this.renderLoginIfNotLoggedIn(<ChatListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/chats/:chatName" render={(props) => this.renderLoginIfNotLoggedIn(<ChatEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/chat" render={(props) => this.renderLoginIfNotLoggedIn(<ChatPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/messages" render={(props) => this.renderLoginIfNotLoggedIn(<MessageListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/messages/:messageName" render={(props) => this.renderLoginIfNotLoggedIn(<MessageEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/plans" render={(props) => this.renderLoginIfNotLoggedIn(<PlanListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/plan/:organizationName/:planName" render={(props) => this.renderLoginIfNotLoggedIn(<PlanEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/pricings" render={(props) => this.renderLoginIfNotLoggedIn(<PricingListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/pricing/:organizationName/:pricingName" render={(props) => this.renderLoginIfNotLoggedIn(<PricingEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/subscriptions" render={(props) => this.renderLoginIfNotLoggedIn(<SubscriptionListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/subscription/:organizationName/:subscriptionName" render={(props) => this.renderLoginIfNotLoggedIn(<SubscriptionEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />
|
||||
@ -651,7 +685,13 @@ class App extends Component {
|
||||
textAlign: "center",
|
||||
}
|
||||
}>
|
||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
|
||||
{
|
||||
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
||||
<React.Fragment>
|
||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</Footer>
|
||||
</React.Fragment>
|
||||
);
|
||||
@ -668,7 +708,8 @@ class App extends Component {
|
||||
window.location.pathname.startsWith("/prompt") ||
|
||||
window.location.pathname.startsWith("/result") ||
|
||||
window.location.pathname.startsWith("/cas") ||
|
||||
window.location.pathname.startsWith("/auto-signup");
|
||||
window.location.pathname.startsWith("/auto-signup") ||
|
||||
window.location.pathname.startsWith("/select-plan");
|
||||
}
|
||||
|
||||
renderPage() {
|
||||
|
@ -112,7 +112,6 @@ class ApplicationEditPage extends React.Component {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getApplication();
|
||||
this.getOrganizations();
|
||||
this.getCerts();
|
||||
this.getProviders();
|
||||
this.getSamlMetadata();
|
||||
}
|
||||
@ -126,6 +125,8 @@ class ApplicationEditPage extends React.Component {
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
|
||||
this.getCerts(application.organization);
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,8 +145,8 @@ class ApplicationEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getCerts() {
|
||||
CertBackend.getCerts(this.props.account.owner)
|
||||
getCerts(owner) {
|
||||
CertBackend.getCerts(owner)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
certs: (res.msg === undefined) ? res : [],
|
||||
@ -790,7 +791,7 @@ class ApplicationEditPage extends React.Component {
|
||||
let signUpUrl = `/signup/${this.state.application.name}`;
|
||||
|
||||
let redirectUri;
|
||||
if (this.state.application.redirectUris.length !== 0) {
|
||||
if (this.state.application.redirectUris?.length > 0) {
|
||||
redirectUri = this.state.application.redirectUris[0];
|
||||
} else {
|
||||
redirectUri = "\"ERROR: You must specify at least one Redirect URL in 'Redirect URLs'\"";
|
||||
|
@ -65,6 +65,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
redirectUris: ["http://localhost:9000/callback"],
|
||||
tokenFormat: "JWT",
|
||||
expireInHours: 24 * 7,
|
||||
refreshExpireInHours: 24 * 7,
|
||||
formOffset: 2,
|
||||
};
|
||||
}
|
||||
@ -175,7 +176,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
// width: '600px',
|
||||
render: (text, record, index) => {
|
||||
const providers = text;
|
||||
if (providers.length === 0) {
|
||||
if (providers === null || providers.length === 0) {
|
||||
return `(${i18next.t("general:empty")})`;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import copy from "copy-to-clipboard";
|
||||
@ -29,6 +30,7 @@ class CertEditPage extends React.Component {
|
||||
this.state = {
|
||||
classes: props,
|
||||
certName: props.match.params.certName,
|
||||
owner: props.match.params.organizationName,
|
||||
cert: null,
|
||||
organizations: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
@ -37,10 +39,11 @@ class CertEditPage extends React.Component {
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getCert();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getCert() {
|
||||
CertBackend.getCert(this.props.account.owner, this.state.certName)
|
||||
CertBackend.getCert(this.state.owner, this.state.certName)
|
||||
.then((cert) => {
|
||||
this.setState({
|
||||
cert: cert,
|
||||
@ -48,6 +51,15 @@ class CertEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parseCertField(key, value) {
|
||||
if (["port"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
@ -230,7 +242,7 @@ class CertEditPage extends React.Component {
|
||||
|
||||
submitCertEdit(willExist) {
|
||||
const cert = Setting.deepCopy(this.state.cert);
|
||||
CertBackend.updateCert(this.state.cert.owner, this.state.certName, cert)
|
||||
CertBackend.updateCert(this.state.owner, this.state.certName, cert)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
@ -241,7 +253,7 @@ class CertEditPage extends React.Component {
|
||||
if (willExist) {
|
||||
this.props.history.push("/certs");
|
||||
} else {
|
||||
this.props.history.push(`/certs/${this.state.cert.name}`);
|
||||
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
|
@ -23,10 +23,20 @@ import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class CertListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
owner: Setting.isAdminUser(this.props.account) ? "admin" : this.props.account.owner,
|
||||
});
|
||||
}
|
||||
|
||||
newCert() {
|
||||
const randomName = Setting.getRandomName();
|
||||
return {
|
||||
owner: this.props.account.owner, // this.props.account.certname,
|
||||
owner: this.state.owner,
|
||||
name: `cert_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Cert - ${randomName}`,
|
||||
@ -45,7 +55,7 @@ class CertListPage extends BaseListPage {
|
||||
CertBackend.addCert(newCert)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/certs/${newCert.name}`, mode: "add"});
|
||||
this.props.history.push({pathname: `/certs/${newCert.owner}/${newCert.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
@ -86,7 +96,7 @@ class CertListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/certs/${text}`}>
|
||||
<Link to={`/certs/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
@ -99,6 +109,9 @@ class CertListPage extends BaseListPage {
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("organization"),
|
||||
render: (text, record, index) => {
|
||||
return (text !== "admin") ? text : i18next.t("provider:admin (Shared)");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
@ -176,7 +189,7 @@ class CertListPage extends BaseListPage {
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/certs/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/certs/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)}
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
|
@ -28,3 +28,5 @@ export const ThemeDefault = {
|
||||
borderRadius: 6,
|
||||
isCompact: false,
|
||||
};
|
||||
|
||||
export const CustomFooter = null;
|
||||
|
@ -16,6 +16,8 @@ import React from "react";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
import {Spin} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import PricingPage from "./pricing/PricingPage";
|
||||
import * as Setting from "./Setting";
|
||||
import * as Conf from "./Conf";
|
||||
import SignupPage from "./auth/SignupPage";
|
||||
@ -33,6 +35,7 @@ class EntryPage extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
application: undefined,
|
||||
pricing: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -65,9 +68,23 @@ class EntryPage extends React.Component {
|
||||
this.props.updataThemeData(themeData);
|
||||
};
|
||||
|
||||
const onUpdatePricing = (pricing) => {
|
||||
this.setState({
|
||||
pricing: pricing,
|
||||
});
|
||||
|
||||
ApplicationBackend.getApplication("admin", pricing.application)
|
||||
.then((application) => {
|
||||
const themeData = application !== null ? Setting.getThemeData(application.organizationObj, application) : Conf.ThemeDefault;
|
||||
this.props.updataThemeData(themeData);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
||||
<Spin size="large" spinning={this.state.application === undefined} tip={i18next.t("login:Loading")} style={{margin: "0 auto"}} />
|
||||
<div className="loginBackground"
|
||||
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
||||
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
||||
style={{margin: "0 auto"}} />
|
||||
<Switch>
|
||||
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
@ -85,6 +102,7 @@ class EntryPage extends React.Component {
|
||||
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} />
|
||||
<Route exact path="/select-plan/:pricingName" render={(props) => this.renderHomeIfLoggedIn(<PricingPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />)} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
@ -185,6 +185,7 @@ class LdapEditPage extends React.Component {
|
||||
{value: "uid", label: "uid"},
|
||||
{value: "mail", label: "Email"},
|
||||
{value: "mobile", label: "mobile"},
|
||||
{value: "sAMAccountName", label: "sAMAccountName"},
|
||||
].map((item) => Setting.getOption(item.label, item.value))} onChange={value => {
|
||||
this.updateLdapField("filterFields", value);
|
||||
}} />
|
||||
|
@ -94,7 +94,7 @@ class LdapSyncPage extends React.Component {
|
||||
if (res.status === "ok") {
|
||||
this.setState((prevState) => {
|
||||
prevState.users = res.data.users;
|
||||
prevState.existUuids = res.data2?.length > 0 ? res.data2 : [];
|
||||
prevState.existUuids = res.data.existUuids?.length > 0 ? res.data.existUuids.filter(uuid => uuid !== "") : [];
|
||||
return prevState;
|
||||
});
|
||||
} else {
|
||||
|
@ -24,6 +24,7 @@ import {LinkOutlined} from "@ant-design/icons";
|
||||
import LdapTable from "./table/LdapTable";
|
||||
import AccountTable from "./table/AccountTable";
|
||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||
import MfaTable from "./table/MfaTable";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -199,6 +200,22 @@ class OrganizationEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Languages"), i18next.t("general:Languages - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}}
|
||||
options={Setting.Countries.map((item) => {
|
||||
return Setting.getOption(item.label, item.key);
|
||||
})}
|
||||
value={this.state.organization.languages ?? []}
|
||||
onChange={(value => {
|
||||
this.updateOrganizationField("languages", value);
|
||||
})} >
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Default avatar"), i18next.t("general:Default avatar - Tooltip"))} :
|
||||
@ -258,22 +275,6 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Languages"), i18next.t("general:Languages - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}}
|
||||
options={Setting.Countries.map((item) => {
|
||||
return Setting.getOption(item.label, item.key);
|
||||
})}
|
||||
value={this.state.organization.languages ?? []}
|
||||
onChange={(value => {
|
||||
this.updateOrganizationField("languages", value);
|
||||
})} >
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Init score"), i18next.t("organization:Init score - Tooltip"))} :
|
||||
@ -316,6 +317,18 @@ class OrganizationEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:MFA items"), i18next.t("general:MFA items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<MfaTable
|
||||
title={i18next.t("general:MFA items")}
|
||||
table={this.state.organization.mfaItems ?? []}
|
||||
onUpdateTable={(value) => {this.updateOrganizationField("mfaItems", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("theme:Theme"), i18next.t("theme:Theme - Tooltip"))} :
|
||||
|
273
web/src/PlanEditPage.js
Normal file
273
web/src/PlanEditPage.js
Normal file
@ -0,0 +1,273 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as RoleBackend from "./backend/RoleBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class PlanEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
planName: props.match.params.planName,
|
||||
plan: null,
|
||||
organizations: [],
|
||||
users: [],
|
||||
roles: [],
|
||||
providers: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getPlan();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getPlan() {
|
||||
PlanBackend.getPlan(this.state.organizationName, this.state.planName)
|
||||
.then((plan) => {
|
||||
this.setState({
|
||||
plan: plan,
|
||||
});
|
||||
|
||||
this.getUsers(plan.owner);
|
||||
this.getRoles(plan.owner);
|
||||
});
|
||||
}
|
||||
|
||||
getRoles(organizationName) {
|
||||
RoleBackend.getRoles(organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
roles: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUsers(organizationName) {
|
||||
UserBackend.getUsers(organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
users: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parsePlanField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updatePlanField(key, value) {
|
||||
value = this.parsePlanField(key, value);
|
||||
|
||||
const plan = this.state.plan;
|
||||
plan[key] = value;
|
||||
this.setState({
|
||||
plan: plan,
|
||||
});
|
||||
}
|
||||
|
||||
renderPlan() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("plan:New Plan") : i18next.t("plan:Edit Plan")}
|
||||
<Button onClick={() => this.submitPlanEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitPlanEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deletePlan()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.owner} onChange={(owner => {
|
||||
this.updatePlanField("owner", owner);
|
||||
this.getUsers(owner);
|
||||
this.getRoles(owner);
|
||||
})}
|
||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.plan.name} onChange={e => {
|
||||
this.updatePlanField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.plan.displayName} onChange={e => {
|
||||
this.updatePlanField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("plan:Sub roles - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.role} onChange={(value => {this.updatePlanField("role", value);})}
|
||||
options={this.state.roles.map((role) => Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.plan.description} onChange={e => {
|
||||
this.updatePlanField("description", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("plan:PricePerMonth"), i18next.t("plan:PricePerMonth - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => {
|
||||
this.updatePlanField("pricePerMonth", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("plan:PricePerYear"), i18next.t("plan:PricePerYear - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.plan.pricePerYear} onChange={value => {
|
||||
this.updatePlanField("pricePerYear", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.currency} onChange={(value => {
|
||||
this.updatePlanField("currency", value);
|
||||
})}>
|
||||
{
|
||||
[
|
||||
{id: "USD", name: "USD"},
|
||||
{id: "CNY", name: "CNY"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.plan.isEnabled} onChange={checked => {
|
||||
this.updatePlanField("isEnabled", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitPlanEdit(willExist) {
|
||||
const plan = Setting.deepCopy(this.state.plan);
|
||||
PlanBackend.updatePlan(this.state.organizationName, this.state.planName, plan)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
planName: this.state.plan.name,
|
||||
});
|
||||
|
||||
if (willExist) {
|
||||
this.props.history.push("/plans");
|
||||
} else {
|
||||
this.props.history.push(`/plan/${this.state.plan.owner}/${this.state.plan.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
this.updatePlanField("name", this.state.planName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deletePlan() {
|
||||
PlanBackend.deletePlan(this.state.plan)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push("/plans");
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.plan !== null ? this.renderPlan() : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitPlanEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitPlanEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deletePlan()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlanEditPage;
|
236
web/src/PlanListPage.js
Normal file
236
web/src/PlanListPage.js
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Switch, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class PlanListPage extends BaseListPage {
|
||||
newPlan() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
|
||||
|
||||
return {
|
||||
owner: owner,
|
||||
name: `plan_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
pricePerMonth: 10,
|
||||
pricePerYear: 100,
|
||||
currency: "USD",
|
||||
displayName: `New Plan - ${randomName}`,
|
||||
};
|
||||
}
|
||||
|
||||
addPlan() {
|
||||
const newPlan = this.newPlan();
|
||||
PlanBackend.addPlan(newPlan)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/plan/${newPlan.owner}/${newPlan.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deletePlan(i) {
|
||||
PlanBackend.deletePlan(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.setState({
|
||||
data: Setting.deleteRow(this.state.data, i),
|
||||
pagination: {total: this.state.pagination.total - 1},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(plans) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "140px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/plans/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price per month"),
|
||||
dataIndex: "pricePerMonth",
|
||||
key: "pricePerMonth",
|
||||
width: "130px",
|
||||
...this.getColumnSearchProps("pricePerMonth"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price per year"),
|
||||
dataIndex: "pricePerYear",
|
||||
key: "pricePerYear",
|
||||
width: "130px",
|
||||
...this.getColumnSearchProps("pricePerYear"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Sub role"),
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("role"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Is enabled"),
|
||||
dataIndex: "isEnabled",
|
||||
key: "isEnabled",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "200px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/plan/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deletePlan(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={plans} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Plans")}
|
||||
<Button type="primary" size="small" onClick={this.addPlan.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
let field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (params.type !== undefined && params.type !== null) {
|
||||
field = "type";
|
||||
value = params.type;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
PlanBackend.getPlans("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default PlanListPage;
|
309
web/src/PricingEditPage.js
Normal file
309
web/src/PricingEditPage.js
Normal file
@ -0,0 +1,309 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {CopyOutlined} from "@ant-design/icons";
|
||||
import copy from "copy-to-clipboard";
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as PricingBackend from "./backend/PricingBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import PricingPage from "./pricing/PricingPage";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class PricingEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
pricingName: props.match.params.pricingName,
|
||||
organizations: [],
|
||||
application: null,
|
||||
applications: [],
|
||||
pricing: null,
|
||||
plans: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getPricing();
|
||||
this.getOrganizations();
|
||||
this.getApplicationsByOrganization(this.state.organizationName);
|
||||
this.getUserApplication();
|
||||
}
|
||||
|
||||
getPricing() {
|
||||
PricingBackend.getPricing(this.state.organizationName, this.state.pricingName)
|
||||
.then((pricing) => {
|
||||
this.setState({
|
||||
pricing: pricing,
|
||||
});
|
||||
this.getPlans(pricing.owner);
|
||||
});
|
||||
}
|
||||
|
||||
getPlans(organizationName) {
|
||||
PlanBackend.getPlans(organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
plans: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parsePricingField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updatePricingField(key, value) {
|
||||
value = this.parsePricingField(key, value);
|
||||
|
||||
const pricing = this.state.pricing;
|
||||
pricing[key] = value;
|
||||
|
||||
this.setState({
|
||||
pricing: pricing,
|
||||
});
|
||||
}
|
||||
|
||||
getApplicationsByOrganization(organizationName) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
applications: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUserApplication() {
|
||||
ApplicationBackend.getUserApplication(this.state.organizationName, this.state.userName)
|
||||
.then((application) => {
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderPricing() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("pricing:New Pricing") : i18next.t("pricing:Edit Pricing")}
|
||||
<Button onClick={() => this.submitPricingEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitPricingEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deletePricing()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.pricing.owner} onChange={(owner => {
|
||||
this.updatePricingField("owner", owner);
|
||||
this.getApplicationsByOrganization(owner);
|
||||
this.getPlans(owner);
|
||||
})}
|
||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.pricing.name} onChange={e => {
|
||||
this.updatePricingField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.pricing.displayName} onChange={e => {
|
||||
this.updatePricingField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.pricing.description} onChange={e => {
|
||||
this.updatePricingField("description", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.pricing.application}
|
||||
onChange={(value => {this.updatePricingField("application", value);})}
|
||||
options={this.state.applications.map((application) => Setting.getOption(application.name, application.name))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("pricing:Sub plans"), i18next.t("Pricing:Sub plans - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select mode="tags" style={{width: "100%"}} value={this.state.pricing.plans}
|
||||
onChange={(value => {
|
||||
this.updatePricingField("plans", value);
|
||||
})}
|
||||
options={this.state.plans.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("pricing:Has trial"), i18next.t("pricing:Has trial - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch disabled={true} checked={this.state.pricing.hasTrial} onChange={checked => {
|
||||
this.updatePricingField("hasTrial", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("pricing:Trial duration"), i18next.t("pricing:Trial duration - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber min={1} value={this.state.pricing.trialDuration} onChange={value => {
|
||||
this.updatePricingField("trialDuration", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.pricing.isEnabled} onChange={checked => {
|
||||
this.updatePricingField("isEnabled", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
|
||||
</Col>
|
||||
{
|
||||
this.renderPreview()
|
||||
}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitPricingEdit(willExist) {
|
||||
const pricing = Setting.deepCopy(this.state.pricing);
|
||||
PricingBackend.updatePricing(this.state.organizationName, this.state.pricingName, pricing)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
pricingName: this.state.pricing.name,
|
||||
});
|
||||
|
||||
if (willExist) {
|
||||
this.props.history.push("/pricings");
|
||||
} else {
|
||||
this.props.history.push(`/pricing/${this.state.pricing.owner}/${this.state.pricing.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
this.updatePricingField("name", this.state.pricingName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deletePricing() {
|
||||
PricingBackend.deletePricing(this.state.pricing)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push("/pricings");
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.pricing !== null ? this.renderPricing() : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitPricingEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitPricingEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deletePricing()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPreview() {
|
||||
const pricingUrl = `/select-plan/${this.state.pricing.name}`;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Col>
|
||||
<Button style={{marginBottom: "10px", marginTop: Setting.isMobile() ? "15px" : "0"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
||||
copy(`${window.location.origin}${pricingUrl}`);
|
||||
Setting.showMessage("success", i18next.t("pricing:pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
|
||||
}}
|
||||
>
|
||||
{i18next.t("pricing:Copy pricing page URL")}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
<PricingPage pricing={this.state.pricing}></PricingPage>
|
||||
</Col>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PricingEditPage;
|
217
web/src/PricingListPage.js
Normal file
217
web/src/PricingListPage.js
Normal file
@ -0,0 +1,217 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Switch, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as PricingBackend from "./backend/PricingBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class PricingListPage extends BaseListPage {
|
||||
newPricing() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
|
||||
|
||||
return {
|
||||
owner: owner,
|
||||
name: `pricing_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
plans: [],
|
||||
displayName: `New Pricing - ${randomName}`,
|
||||
hasTrial: true,
|
||||
isEnabled: true,
|
||||
trialDuration: 14,
|
||||
};
|
||||
}
|
||||
|
||||
addPricing() {
|
||||
const newPricing = this.newPricing();
|
||||
PricingBackend.addPricing(newPricing)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/pricing/${newPricing.owner}/${newPricing.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deletePricing(i) {
|
||||
PricingBackend.deletePricing(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.setState({
|
||||
data: Setting.deleteRow(this.state.data, i),
|
||||
pagination: {total: this.state.pagination.total - 1},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(pricings) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "140px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/pricing/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
|
||||
{
|
||||
title: i18next.t("general:Is enabled"),
|
||||
dataIndex: "isEnabled",
|
||||
key: "isEnabled",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "230px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/pricing/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deletePricing(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={pricings} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Pricings")}
|
||||
<Button type="primary" size="small" onClick={this.addPricing.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
let field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (params.type !== undefined && params.type !== null) {
|
||||
field = "type";
|
||||
value = params.type;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
PricingBackend.getPricings("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default PricingListPage;
|
@ -112,6 +112,9 @@ class ProviderListPage extends BaseListPage {
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("organization"),
|
||||
render: (text, record, index) => {
|
||||
return (text !== "admin") ? text : i18next.t("provider:admin (Shared)");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
|
@ -99,6 +99,21 @@ class ResourceListPage extends BaseListPage {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Application"),
|
||||
dataIndex: "application",
|
||||
@ -288,7 +303,7 @@ class ResourceListPage extends BaseListPage {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
ResourceBackend.getResources("admin", this.props.account.name, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
ResourceBackend.getResources(Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner, this.props.account.name, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
|
@ -42,6 +42,7 @@ export const Countries = [{label: "English", key: "en", country: "US", alt: "Eng
|
||||
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
|
||||
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
|
||||
{label: "TiếngViệt", key: "vi", country: "VN", alt: "TiếngViệt"},
|
||||
{label: "Português", key: "pt", country: "BR", alt: "Português"},
|
||||
];
|
||||
|
||||
export function getThemeData(organization, application) {
|
||||
@ -325,19 +326,19 @@ export function isSignupItemPrompted(signupItem) {
|
||||
}
|
||||
|
||||
export function getAllPromptedProviderItems(application) {
|
||||
return application.providers.filter(providerItem => isProviderPrompted(providerItem));
|
||||
return application.providers?.filter(providerItem => isProviderPrompted(providerItem));
|
||||
}
|
||||
|
||||
export function getAllPromptedSignupItems(application) {
|
||||
return application.signupItems.filter(signupItem => isSignupItemPrompted(signupItem));
|
||||
return application.signupItems?.filter(signupItem => isSignupItemPrompted(signupItem));
|
||||
}
|
||||
|
||||
export function getSignupItem(application, itemName) {
|
||||
const signupItems = application.signupItems?.filter(signupItem => signupItem.name === itemName);
|
||||
if (signupItems.length === 0) {
|
||||
return null;
|
||||
if (signupItems?.length > 0) {
|
||||
return signupItems[0];
|
||||
}
|
||||
return signupItems[0];
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isValidPersonName(personName) {
|
||||
@ -409,12 +410,12 @@ export function isAffiliationPrompted(application) {
|
||||
|
||||
export function hasPromptPage(application) {
|
||||
const providerItems = getAllPromptedProviderItems(application);
|
||||
if (providerItems.length !== 0) {
|
||||
if (providerItems?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const signupItems = getAllPromptedSignupItems(application);
|
||||
if (signupItems.length !== 0) {
|
||||
if (signupItems?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
333
web/src/SubscriptionEditPage.js
Normal file
333
web/src/SubscriptionEditPage.js
Normal file
@ -0,0 +1,333 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import {Button, Card, Col, DatePicker, Input, InputNumber, Row, Select, Switch} from "antd";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class SubscriptionEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
subscriptionName: props.match.params.subscriptionName,
|
||||
subscription: null,
|
||||
organizations: [],
|
||||
users: [],
|
||||
planes: [],
|
||||
providers: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getSubscription();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getSubscription() {
|
||||
SubscriptionBackend.getSubscription(this.state.organizationName, this.state.subscriptionName)
|
||||
.then((subscription) => {
|
||||
this.setState({
|
||||
subscription: subscription,
|
||||
});
|
||||
|
||||
this.getUsers(subscription.owner);
|
||||
this.getPlanes(subscription.owner);
|
||||
});
|
||||
}
|
||||
|
||||
getPlanes(organizationName) {
|
||||
PlanBackend.getPlans(organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
planes: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUsers(organizationName) {
|
||||
UserBackend.getUsers(organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
users: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parseSubscriptionField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateSubscriptionField(key, value) {
|
||||
value = this.parseSubscriptionField(key, value);
|
||||
|
||||
const subscription = this.state.subscription;
|
||||
subscription[key] = value;
|
||||
this.setState({
|
||||
subscription: subscription,
|
||||
});
|
||||
}
|
||||
|
||||
renderSubscription() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("subscription:New Subscription") : i18next.t("subscription:Edit Subscription")}
|
||||
<Button onClick={() => this.submitSubscriptionEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitSubscriptionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteSubscription()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.owner} onChange={(owner => {
|
||||
this.updateSubscriptionField("owner", owner);
|
||||
this.getUsers(owner);
|
||||
this.getPlanes(owner);
|
||||
})}
|
||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.subscription.name} onChange={e => {
|
||||
this.updateSubscriptionField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.subscription.displayName} onChange={e => {
|
||||
this.updateSubscriptionField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:Duration"), i18next.t("subscription:Duration - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.subscription.duration} onChange={value => {
|
||||
this.updateSubscriptionField("duration", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:Start Date"), i18next.t("subscription:Start Date - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<DatePicker value={dayjs(this.state.subscription.startDate)} onChange={value => {
|
||||
this.updateSubscriptionField("startDate", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:End Date"), i18next.t("subscription:End Date - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<DatePicker value={dayjs(this.state.subscription.endDate)} onChange={value => {
|
||||
this.updateSubscriptionField("endDate", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:Sub users"), i18next.t("subscription:Sub users - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select style={{width: "100%"}} value={this.state.subscription.user}
|
||||
onChange={(value => {this.updateSubscriptionField("user", value);})}
|
||||
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:Sub plan"), i18next.t("subscription:Sub plan - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.plan} onChange={(value => {this.updateSubscriptionField("plan", value);})}
|
||||
options={this.state.planes.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.subscription.description} onChange={e => {
|
||||
this.updateSubscriptionField("description", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.subscription.isEnabled} onChange={checked => {
|
||||
this.updateSubscriptionField("isEnabled", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Submitter"), i18next.t("general:Submitter - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.subscription.submitter} onChange={e => {
|
||||
this.updateSubscriptionField("submitter", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Approver"), i18next.t("general:Approver - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.subscription.approver} onChange={e => {
|
||||
this.updateSubscriptionField("approver", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Approve time"), i18next.t("general:Approve time - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={Setting.getFormattedDate(this.state.subscription.approveTime)} onChange={e => {
|
||||
this.updatePermissionField("approveTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} disabled={!Setting.isLocalAdminUser(this.props.account)} style={{width: "100%"}} value={this.state.subscription.state} onChange={(value => {
|
||||
if (this.state.subscription.state !== value) {
|
||||
if (value === "Approved") {
|
||||
this.updateSubscriptionField("approver", this.props.account.name);
|
||||
this.updateSubscriptionField("approveTime", moment().format());
|
||||
} else {
|
||||
this.updateSubscriptionField("approver", "");
|
||||
this.updateSubscriptionField("approveTime", "");
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSubscriptionField("state", value);
|
||||
})}
|
||||
options={[
|
||||
{value: "Approved", name: i18next.t("subscription:Approved")},
|
||||
{value: "Pending", name: i18next.t("subscription:Pending")},
|
||||
].map((item) => Setting.getOption(item.name, item.value))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitSubscriptionEdit(willExist) {
|
||||
const subscription = Setting.deepCopy(this.state.subscription);
|
||||
SubscriptionBackend.updateSubscription(this.state.organizationName, this.state.subscriptionName, subscription)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
subscriptionName: this.state.subscription.name,
|
||||
});
|
||||
|
||||
if (willExist) {
|
||||
this.props.history.push("/subscriptions");
|
||||
} else {
|
||||
this.props.history.push(`/subscription/${this.state.subscription.owner}/${this.state.subscription.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
this.updateSubscriptionField("name", this.state.subscriptionName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSubscription() {
|
||||
SubscriptionBackend.deleteSubscription(this.state.subscription)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push("/subscriptions");
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.subscription !== null ? this.renderSubscription() : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitSubscriptionEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitSubscriptionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteSubscription()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SubscriptionEditPage;
|
239
web/src/SubscriptionListPage.js
Normal file
239
web/src/SubscriptionListPage.js
Normal file
@ -0,0 +1,239 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class SubscriptionListPage extends BaseListPage {
|
||||
newSubscription() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
|
||||
const defaultDuration = 365;
|
||||
|
||||
return {
|
||||
owner: owner,
|
||||
name: `subscription_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
startDate: moment().format(),
|
||||
endDate: moment().add(defaultDuration, "d").format(),
|
||||
displayName: `New Subscription - ${randomName}`,
|
||||
tag: "",
|
||||
users: [],
|
||||
expireInDays: defaultDuration,
|
||||
submitter: this.props.account.name,
|
||||
approver: "",
|
||||
approveTime: "",
|
||||
state: "Pending",
|
||||
};
|
||||
}
|
||||
|
||||
addSubscription() {
|
||||
const newSubscription = this.newSubscription();
|
||||
SubscriptionBackend.addSubscription(newSubscription)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/subscription/${newSubscription.owner}/${newSubscription.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSubscription(i) {
|
||||
SubscriptionBackend.deleteSubscription(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.setState({
|
||||
data: Setting.deleteRow(this.state.data, i),
|
||||
pagination: {total: this.state.pagination.total - 1},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(subscriptions) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "140px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/subscriptions/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Duration"),
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("duration"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Sub plane"),
|
||||
dataIndex: "plan",
|
||||
key: "plan",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("plan"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Sub user"),
|
||||
dataIndex: "user",
|
||||
key: "user",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("user"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
dataIndex: "state",
|
||||
key: "state",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("state"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "230px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/subscription/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteSubscription(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={subscriptions} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Subscriptions")}
|
||||
<Button type="primary" size="small" onClick={this.addSubscription.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
let field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (params.type !== undefined && params.type !== null) {
|
||||
field = "type";
|
||||
value = params.type;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
SubscriptionBackend.getSubscriptions("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SubscriptionListPage;
|
@ -28,6 +28,20 @@ require("codemirror/mode/javascript/javascript");
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const applicationTemplate = {
|
||||
owner: "admin", // this.props.account.applicationName,
|
||||
name: "application_123",
|
||||
organization: "built-in",
|
||||
createdTime: "2022-01-01T01:03:42+08:00",
|
||||
displayName: "New Application - 123",
|
||||
logo: `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`,
|
||||
enablePassword: true,
|
||||
enableSignUp: true,
|
||||
enableSigninSession: false,
|
||||
enableCodeSignin: false,
|
||||
enableSamlCompress: false,
|
||||
};
|
||||
|
||||
const previewTemplate = {
|
||||
"id": 9078,
|
||||
"owner": "built-in",
|
||||
@ -37,9 +51,10 @@ const previewTemplate = {
|
||||
"clientIp": "159.89.126.192",
|
||||
"user": "admin",
|
||||
"method": "POST",
|
||||
"requestUri": "/api/login",
|
||||
"requestUri": "/api/add-application",
|
||||
"action": "login",
|
||||
"isTriggered": false,
|
||||
"object": JSON.stringify(applicationTemplate),
|
||||
};
|
||||
|
||||
const userTemplate = {
|
||||
@ -49,7 +64,7 @@ const userTemplate = {
|
||||
"updatedTime": "",
|
||||
"id": "9eb20f79-3bb5-4e74-99ac-39e3b9a171e8",
|
||||
"type": "normal-user",
|
||||
"password": "123",
|
||||
"password": "***",
|
||||
"passwordSalt": "",
|
||||
"displayName": "Admin",
|
||||
"avatar": "https://cdn.casbin.com/usercontent/admin/avatar/1596241359.png",
|
||||
@ -244,7 +259,7 @@ class WebhookEditPage extends React.Component {
|
||||
}} >
|
||||
{
|
||||
(
|
||||
["signup", "login", "logout", "add-user", "update-user", "delete-user", "add-organization", "update-organization", "delete-organization", "add-application", "update-application", "delete-application", "add-provider", "update-provider", "delete-provider"].map((option, index) => {
|
||||
["signup", "login", "logout", "add-user", "update-user", "delete-user", "add-organization", "update-organization", "delete-organization", "add-application", "update-application", "delete-application", "add-provider", "update-provider", "delete-provider", "update-subscription"].map((option, index) => {
|
||||
return (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
);
|
||||
|
@ -15,7 +15,7 @@
|
||||
import {authConfig} from "./Auth";
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getAccount(query) {
|
||||
export function getAccount(query = "") {
|
||||
return fetch(`${authConfig.serverUrl}/api/get-account${query}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
@ -33,7 +33,7 @@ import LanguageSelect from "../common/select/LanguageSelect";
|
||||
import {CaptchaModal} from "../common/modal/CaptchaModal";
|
||||
import {CaptchaRule} from "../common/modal/CaptchaModal";
|
||||
import RedirectForm from "../common/RedirectForm";
|
||||
import {MfaAuthVerifyForm, NextMfa} from "./MfaAuthVerifyForm";
|
||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./MfaAuthVerifyForm";
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props) {
|
||||
@ -224,23 +224,27 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
postCodeLoginAction(res) {
|
||||
postCodeLoginAction(resp) {
|
||||
const application = this.getApplicationObj();
|
||||
const ths = this;
|
||||
const oAuthParams = Util.getOAuthGetParameters();
|
||||
const code = res.data;
|
||||
const code = resp.data;
|
||||
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
|
||||
const noRedirect = oAuthParams.noRedirect;
|
||||
if (Setting.hasPromptPage(application)) {
|
||||
AuthBackend.getAccount("")
|
||||
.then((res) => {
|
||||
let account = null;
|
||||
if (res.status === "ok") {
|
||||
account = res.data;
|
||||
account.organization = res.data2;
|
||||
|
||||
if (Setting.hasPromptPage(application) || resp.msg === RequiredMfa) {
|
||||
AuthBackend.getAccount()
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const account = res.data;
|
||||
account.organization = res.data2;
|
||||
this.onUpdateAccount(account);
|
||||
|
||||
if (resp.msg === RequiredMfa) {
|
||||
Setting.goToLink(`/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}&promptType=mfa`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Setting.isPromptAnswered(account, application)) {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
} else {
|
||||
@ -328,10 +332,20 @@ class LoginPage extends React.Component {
|
||||
const responseType = values["type"];
|
||||
|
||||
if (responseType === "login") {
|
||||
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
|
||||
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
if (res.msg === RequiredMfa) {
|
||||
AuthBackend.getAccount().then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const account = res.data;
|
||||
account.organization = res.data2;
|
||||
this.onUpdateAccount(account);
|
||||
}
|
||||
});
|
||||
Setting.goToLink(`/prompt/${this.getApplicationObj().name}?promptType=mfa`);
|
||||
} else {
|
||||
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
}
|
||||
} else if (responseType === "code") {
|
||||
this.postCodeLoginAction(res);
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
@ -352,6 +366,7 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (res.status === "ok") {
|
||||
callback(res);
|
||||
} else if (res.status === NextMfa) {
|
||||
@ -423,6 +438,7 @@ class LoginPage extends React.Component {
|
||||
<Form
|
||||
name="normal_login"
|
||||
initialValues={{
|
||||
|
||||
organization: application.organization,
|
||||
application: application.name,
|
||||
autoSignin: true,
|
||||
@ -559,7 +575,7 @@ class LoginPage extends React.Component {
|
||||
</div>
|
||||
<br />
|
||||
{
|
||||
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
|
||||
application.providers?.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
|
||||
return ProviderButton.renderProviderLogo(providerItem.provider, application, 40, 10, "big", this.props.location);
|
||||
})
|
||||
}
|
||||
@ -818,7 +834,7 @@ class LoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const visibleOAuthProviderItems = application.providers.filter(providerItem => this.isProviderVisible(providerItem));
|
||||
const visibleOAuthProviderItems = (application.providers === null) ? [] : application.providers.filter(providerItem => this.isProviderVisible(providerItem));
|
||||
if (this.props.preview !== "auto" && !application.enablePassword && visibleOAuthProviderItems.length === 1) {
|
||||
Setting.goToLink(Provider.getAuthUrl(application, visibleOAuthProviderItems[0].provider, "signup"));
|
||||
return (
|
||||
|
@ -20,6 +20,7 @@ import {SmsMfaType} from "./MfaSetupPage";
|
||||
import {MfaSmsVerifyForm} from "./MfaVerifyForm";
|
||||
|
||||
export const NextMfa = "NextMfa";
|
||||
export const RequiredMfa = "RequiredMfa";
|
||||
|
||||
export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, application, onSuccess, onFail}) {
|
||||
formValues.password = "";
|
||||
|
@ -97,9 +97,9 @@ export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail})
|
||||
});
|
||||
};
|
||||
|
||||
if (mfaProps.type === SmsMfaType) {
|
||||
if (mfaProps?.type === SmsMfaType) {
|
||||
return <MfaSmsVerifyForm onFinish={onFinish} application={application} />;
|
||||
} else if (mfaProps.type === TotpMfaType) {
|
||||
} else if (mfaProps?.type === TotpMfaType) {
|
||||
return <MfaTotpVerifyForm onFinish={onFinish} mfaProps={mfaProps} />;
|
||||
} else {
|
||||
return <div></div>;
|
||||
@ -145,7 +145,11 @@ class MfaSetupPage extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
account: props.account,
|
||||
current: 0,
|
||||
applicationName: (props.applicationName ?? props.account?.signupApplication) ?? "",
|
||||
isAuthenticated: props.isAuthenticated ?? false,
|
||||
isPromptPage: props.isPromptPage,
|
||||
redirectUri: props.redirectUri,
|
||||
current: props.current ?? 0,
|
||||
type: props.type ?? SmsMfaType,
|
||||
mfaProps: null,
|
||||
};
|
||||
@ -155,8 +159,25 @@ class MfaSetupPage extends React.Component {
|
||||
this.getApplication();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
if (prevState.isAuthenticated === true && this.state.mfaProps === null) {
|
||||
MfaBackend.MfaSetupInitiate({
|
||||
type: this.state.type,
|
||||
...this.getUser(),
|
||||
}).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
mfaProps: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
ApplicationBackend.getApplication("admin", this.state.account.signupApplication)
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((application) => {
|
||||
if (application !== null) {
|
||||
this.setState({
|
||||
@ -181,18 +202,9 @@ class MfaSetupPage extends React.Component {
|
||||
return <CheckPasswordForm
|
||||
user={this.getUser()}
|
||||
onSuccess={() => {
|
||||
MfaBackend.MfaSetupInitiate({
|
||||
type: this.state.type,
|
||||
...this.getUser(),
|
||||
}).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
current: this.state.current + 1,
|
||||
mfaProps: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
|
||||
}
|
||||
this.setState({
|
||||
current: this.state.current + 1,
|
||||
isAuthenticated: true,
|
||||
});
|
||||
}}
|
||||
onFail={(res) => {
|
||||
@ -200,8 +212,12 @@ class MfaSetupPage extends React.Component {
|
||||
}}
|
||||
/>;
|
||||
case 1:
|
||||
if (!this.state.isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MfaVerifyForm
|
||||
mfaProps={{...this.state.mfaProps}}
|
||||
mfaProps={this.state.mfaProps}
|
||||
application={this.state.application}
|
||||
user={this.getUser()}
|
||||
onSuccess={() => {
|
||||
@ -214,10 +230,18 @@ class MfaSetupPage extends React.Component {
|
||||
}}
|
||||
/>;
|
||||
case 2:
|
||||
if (!this.state.isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <EnableMfaForm user={this.getUser()} mfaProps={{type: this.state.type, ...this.state.mfaProps}}
|
||||
onSuccess={() => {
|
||||
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
if (this.state.isPromptPage && this.state.redirectUri) {
|
||||
Setting.goToLink(this.state.redirectUri);
|
||||
} else {
|
||||
Setting.goToLink("/account");
|
||||
}
|
||||
}}
|
||||
onFail={(res) => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to enable")}: ${res.msg}`);
|
||||
@ -265,7 +289,9 @@ class MfaSetupPage extends React.Component {
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
|
||||
<div style={{marginTop: "10px", textAlign: "center"}}>{this.renderStep()}</div>
|
||||
<div style={{marginTop: "10px", textAlign: "center"}}>
|
||||
{this.renderStep()}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
@ -18,7 +18,6 @@ import {CopyOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import {SendCodeInput} from "../common/SendCodeInput";
|
||||
import * as Setting from "../Setting";
|
||||
import React from "react";
|
||||
import QRCode from "qrcode.react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
|
||||
|
||||
@ -105,7 +104,6 @@ export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => {
|
||||
>
|
||||
<Row type="flex" justify="center" align="middle">
|
||||
<Col>
|
||||
<QRCode value={mfaProps.url} size={200} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
@ -23,16 +23,19 @@ import AffiliationSelect from "../common/select/AffiliationSelect";
|
||||
import OAuthWidget from "../common/OAuthWidget";
|
||||
import RegionSelect from "../common/select/RegionSelect";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import MfaSetupPage from "./MfaSetupPage";
|
||||
|
||||
class PromptPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = new URLSearchParams(this.props.location.search);
|
||||
this.state = {
|
||||
classes: props,
|
||||
type: props.type,
|
||||
applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName),
|
||||
application: null,
|
||||
user: null,
|
||||
promptType: params.get("promptType"),
|
||||
};
|
||||
}
|
||||
|
||||
@ -225,6 +228,26 @@ class PromptPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
renderPromptProvider(application) {
|
||||
return <>
|
||||
{this.renderContent(application)}
|
||||
<div style={{marginTop: "50px"}}>
|
||||
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button>
|
||||
</div>;
|
||||
</>;
|
||||
}
|
||||
|
||||
renderPromptMfa() {
|
||||
return <MfaSetupPage
|
||||
applicationName={this.getApplicationObj().name}
|
||||
account={this.props.account}
|
||||
current={1}
|
||||
isAuthenticated={true}
|
||||
isPromptPage={true}
|
||||
redirectUri={this.getRedirectUrl()}
|
||||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const application = this.getApplicationObj();
|
||||
if (application === null) {
|
||||
@ -259,12 +282,7 @@ class PromptPage extends React.Component {
|
||||
{
|
||||
Setting.renderLogo(application)
|
||||
}
|
||||
{
|
||||
this.renderContent(application)
|
||||
}
|
||||
<div style={{marginTop: "50px"}}>
|
||||
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button>
|
||||
</div>
|
||||
{this.state.promptType !== "mfa" ? this.renderPromptProvider(application) : this.renderPromptMfa(application)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -165,6 +165,11 @@ class SignupPage extends React.Component {
|
||||
|
||||
onFinish(values) {
|
||||
const application = this.getApplicationObj();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
values["plan"] = params.get("plan");
|
||||
values["pricing"] = params.get("pricing");
|
||||
|
||||
AuthBackend.signup(values)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
|
@ -86,7 +86,13 @@ export function getOAuthGetParameters(params) {
|
||||
const responseType = getRefinedValue(queries.get("response_type"));
|
||||
const redirectUri = getRefinedValue(queries.get("redirect_uri"));
|
||||
const scope = getRefinedValue(queries.get("scope"));
|
||||
const state = getRefinedValue(queries.get("state"));
|
||||
|
||||
let state = getRefinedValue(queries.get("state"));
|
||||
if (state.startsWith("/auth/oauth2/login.php?wantsurl=")) {
|
||||
// state contains URL param encoding for Moodle, URLSearchParams automatically decoded it, so here encode it again
|
||||
state = encodeURIComponent(state);
|
||||
}
|
||||
|
||||
const nonce = getRefinedValue(queries.get("nonce"));
|
||||
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
|
||||
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
|
||||
|
81
web/src/backend/PlanBackend.js
Normal file
81
web/src/backend/PlanBackend.js
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getPlans(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-plans?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getPlanById(id, includeOption = false) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${id}&includeOption=${includeOption}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getPlan(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updatePlan(owner, name, plan) {
|
||||
const newPlan = Setting.deepCopy(plan);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-plan?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newPlan),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addPlan(plan) {
|
||||
const newPlan = Setting.deepCopy(plan);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-plan`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newPlan),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deletePlan(plan) {
|
||||
const newPlan = Setting.deepCopy(plan);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-plan`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newPlan),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
71
web/src/backend/PricingBackend.js
Normal file
71
web/src/backend/PricingBackend.js
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getPricings(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-pricings?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getPricing(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-pricing?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updatePricing(owner, name, pricing) {
|
||||
const newPricing = Setting.deepCopy(pricing);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-pricing?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newPricing),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addPricing(pricing) {
|
||||
const newPricing = Setting.deepCopy(pricing);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-pricing`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newPricing),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deletePricing(pricing) {
|
||||
const newPricing = Setting.deepCopy(pricing);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-pricing`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newPricing),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
71
web/src/backend/SubscriptionBackend.js
Normal file
71
web/src/backend/SubscriptionBackend.js
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getSubscriptions(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-subscriptions?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getSubscription(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-subscription?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateSubscription(owner, name, subscription) {
|
||||
const newSubscription = Setting.deepCopy(subscription);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-subscription?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSubscription),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addSubscription(subscription) {
|
||||
const newSubscription = Setting.deepCopy(subscription);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-subscription`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSubscription),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteSubscription(subscription) {
|
||||
const newSubscription = Setting.deepCopy(subscription);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-subscription`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newSubscription),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
@ -23,6 +23,7 @@ import ja from "./locales/ja/data.json";
|
||||
import ko from "./locales/ko/data.json";
|
||||
import ru from "./locales/ru/data.json";
|
||||
import vi from "./locales/vi/data.json";
|
||||
import pt from "./locales/pt/data.json";
|
||||
import * as Conf from "./Conf";
|
||||
import {initReactI18next} from "react-i18next";
|
||||
|
||||
@ -37,6 +38,7 @@ const resources = {
|
||||
ko: ko,
|
||||
ru: ru,
|
||||
vi: vi,
|
||||
pt: pt,
|
||||
};
|
||||
|
||||
function initLanguage() {
|
||||
@ -83,6 +85,9 @@ function initLanguage() {
|
||||
case "vi":
|
||||
language = "vi";
|
||||
break;
|
||||
case "pt":
|
||||
language = "pt";
|
||||
break;
|
||||
default:
|
||||
language = Conf.DefaultLanguage;
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "Telefon",
|
||||
"Phone - Tooltip": "Telefonnummer",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "Pläne",
|
||||
"Pricings": "Preise",
|
||||
"Preview": "Vorschau",
|
||||
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
|
||||
"Products": "Produkte",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Es tut uns leid, aber Sie haben keine Berechtigung, auf diese Seite zuzugreifen, oder Sie sind nicht angemeldet.",
|
||||
"State": "Bundesland / Staat",
|
||||
"State - Tooltip": "Bundesland",
|
||||
"Subscriptions": "Abonnements",
|
||||
"Successfully added": "Erfolgreich hinzugefügt",
|
||||
"Successfully deleted": "Erfolgreich gelöscht",
|
||||
"Successfully saved": "Erfolgreich gespeichert",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "HTTP Methode",
|
||||
"New Webhook": "Neue Webhook",
|
||||
"Value": "Wert"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "Rolle im aktuellen Plan enthalten",
|
||||
"PricePerMonth": "Preis pro Monat",
|
||||
"PricePerYear": "Preis pro Jahr",
|
||||
"PerMonth": "pro Monat"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "Zusatzpläne",
|
||||
"Sub plans - Tooltip": "Pläne im aktuellen Preismodell enthalten",
|
||||
"Has trial": "Testphase verfügbar",
|
||||
"Has trial - Tooltip": "Verfügbarkeit der Testphase nach Auswahl eines Plans",
|
||||
"Trial duration": "Testphase Dauer",
|
||||
"Trial duration - Tooltip": "Dauer der Testphase",
|
||||
"Getting started": "Loslegen",
|
||||
"Copy pricing page URL": "Preisseite URL kopieren",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Preisseite URL erfolgreich in die Zwischenablage kopiert. Bitte fügen Sie sie in ein Inkognito-Fenster oder einen anderen Browser ein.",
|
||||
"days trial available!": "Tage Testphase verfügbar!",
|
||||
"Free": "Kostenlos"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "Laufzeit",
|
||||
"Duration - Tooltip": "Laufzeit des Abonnements",
|
||||
"Start Date": "Startdatum",
|
||||
"Start Date - Tooltip": "Startdatum",
|
||||
"End Date": "Enddatum",
|
||||
"End Date - Tooltip": "Enddatum",
|
||||
"Sub users": "Abonnenten",
|
||||
"Sub users - Tooltip": "Abonnenten",
|
||||
"Sub plan": "Abonnementplan",
|
||||
"Sub plan - Tooltip": "Abonnementplan"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "Phone",
|
||||
"Phone - Tooltip": "Phone number",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "Plans",
|
||||
"Pricings": "Pricings",
|
||||
"Preview": "Preview",
|
||||
"Preview - Tooltip": "Preview the configured effects",
|
||||
"Products": "Products",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State",
|
||||
"Subscriptions": "Subscriptions",
|
||||
"Successfully added": "Successfully added",
|
||||
"Successfully deleted": "Successfully deleted",
|
||||
"Successfully saved": "Successfully saved",
|
||||
@ -884,5 +887,37 @@
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Value": "Value"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "Role included in the current plane",
|
||||
"PricePerMonth": "Price per month",
|
||||
"PricePerYear": "Price per year",
|
||||
"PerMonth": "per month"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "Sub plans",
|
||||
"Sub plans - Tooltip": "Plans included in the current pricing",
|
||||
"Has trial": "Has trial",
|
||||
"Has trial - Tooltip": "Availability of the trial period after choosing a plan",
|
||||
"Trial duration": "Trial duration",
|
||||
"Trial duration - Tooltip": "Trial duration period",
|
||||
"Getting started" : "Getting started",
|
||||
"Copy pricing page URL": "Copy pricing page URL",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser" : "pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
|
||||
"days trial available!": "days trial available!",
|
||||
"Free": "Free"
|
||||
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "Duration",
|
||||
"Duration - Tooltip": "Subscription duration",
|
||||
"Start Date": "Start Date",
|
||||
"Start Date - Tooltip": "Start Date",
|
||||
"End Date": "End Date",
|
||||
"End Date - Tooltip": "End Date",
|
||||
"Sub users": "Sub users",
|
||||
"Sub users - Tooltip": "Sub users",
|
||||
"Sub plan": "Sub plan",
|
||||
"Sub plan - Tooltip": "Sub plan"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "Teléfono",
|
||||
"Phone - Tooltip": "Número de teléfono",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "Planes",
|
||||
"Pricings": "Precios",
|
||||
"Preview": "Avance",
|
||||
"Preview - Tooltip": "Vista previa de los efectos configurados",
|
||||
"Products": "Productos",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Lo siento, no tiene permiso para acceder a esta página o su estado de inicio de sesión es inválido.",
|
||||
"State": "Estado",
|
||||
"State - Tooltip": "Estado",
|
||||
"Subscriptions": "Suscripciones",
|
||||
"Successfully added": "Éxito al agregar",
|
||||
"Successfully deleted": "Éxito en la eliminación",
|
||||
"Successfully saved": "Guardado exitosamente",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "Método HTTP",
|
||||
"New Webhook": "Nuevo Webhook",
|
||||
"Value": "Valor"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "Rol incluido en el plan actual",
|
||||
"PricePerMonth": "Precio por mes",
|
||||
"PricePerYear": "Precio por año",
|
||||
"PerMonth": "por mes"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "Planes adicionales",
|
||||
"Sub plans - Tooltip": "Planes incluidos en la tarifa actual",
|
||||
"Has trial": "Tiene período de prueba",
|
||||
"Has trial - Tooltip": "Disponibilidad del período de prueba después de elegir un plan",
|
||||
"Trial duration": "Duración del período de prueba",
|
||||
"Trial duration - Tooltip": "Duración del período de prueba",
|
||||
"Getting started": "Empezar",
|
||||
"Copy pricing page URL": "Copiar URL de la página de precios",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL de la página de precios copiada correctamente al portapapeles, péguela en una ventana de incógnito u otro navegador",
|
||||
"days trial available!": "días de prueba disponibles",
|
||||
"Free": "Gratis"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "Duración",
|
||||
"Duration - Tooltip": "Duración de la suscripción",
|
||||
"Start Date": "Fecha de inicio",
|
||||
"Start Date - Tooltip": "Fecha de inicio",
|
||||
"End Date": "Fecha de finalización",
|
||||
"End Date - Tooltip": "Fecha de finalización",
|
||||
"Sub users": "Usuarios de la suscripción",
|
||||
"Sub users - Tooltip": "Usuarios de la suscripción",
|
||||
"Sub plan": "Plan de suscripción",
|
||||
"Sub plan - Tooltip": "Plan de suscripción"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "Téléphone",
|
||||
"Phone - Tooltip": "Numéro de téléphone",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "Plans",
|
||||
"Pricings": "Tarifs",
|
||||
"Preview": "Aperçu",
|
||||
"Preview - Tooltip": "Prévisualisez les effets configurés",
|
||||
"Products": "Produits",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Désolé, vous n'avez pas la permission d'accéder à cette page ou votre statut de connexion est invalide.",
|
||||
"State": "État",
|
||||
"State - Tooltip": "État",
|
||||
"Subscriptions": "Abonnements",
|
||||
"Successfully added": "Ajouté avec succès",
|
||||
"Successfully deleted": "Supprimé avec succès",
|
||||
"Successfully saved": "Succès enregistré",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "Méthode HTTP",
|
||||
"New Webhook": "Nouveau webhook",
|
||||
"Value": "Valeur"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "Rôle inclus dans le plan actuel",
|
||||
"PricePerMonth": "Prix par mois",
|
||||
"PricePerYear": "Prix par an",
|
||||
"PerMonth": "par mois"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "Forfaits supplémentaires",
|
||||
"Sub plans - Tooltip": "Forfaits inclus dans la tarification actuelle",
|
||||
"Has trial": "Essai gratuit disponible",
|
||||
"Has trial - Tooltip": "Disponibilité de la période d'essai après avoir choisi un forfait",
|
||||
"Trial duration": "Durée de l'essai",
|
||||
"Trial duration - Tooltip": "Durée de la période d'essai",
|
||||
"Getting started": "Commencer",
|
||||
"Copy pricing page URL": "Copier l'URL de la page tarifs",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL de la page tarifs copiée avec succès dans le presse-papiers, veuillez le coller dans une fenêtre de navigation privée ou un autre navigateur",
|
||||
"days trial available!": "jours d'essai disponibles !",
|
||||
"Free": "Gratuit"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "Durée",
|
||||
"Duration - Tooltip": "Durée de l'abonnement",
|
||||
"Start Date": "Date de début",
|
||||
"Start Date - Tooltip": "Date de début",
|
||||
"End Date": "Date de fin",
|
||||
"End Date - Tooltip": "Date de fin",
|
||||
"Sub users": "Utilisateurs de l'abonnement",
|
||||
"Sub users - Tooltip": "Utilisateurs de l'abonnement",
|
||||
"Sub plan": "Plan de l'abonnement",
|
||||
"Sub plan - Tooltip": "Plan de l'abonnement"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "Telepon",
|
||||
"Phone - Tooltip": "Nomor telepon",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "Rencana",
|
||||
"Pricings": "Harga",
|
||||
"Preview": "Tinjauan",
|
||||
"Preview - Tooltip": "Mengawali pratinjau efek yang sudah dikonfigurasi",
|
||||
"Products": "Produk",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Maaf, Anda tidak memiliki izin untuk mengakses halaman ini atau status masuk tidak valid.",
|
||||
"State": "Negara",
|
||||
"State - Tooltip": "Negara",
|
||||
"Subscriptions": "Langganan",
|
||||
"Successfully added": "Berhasil ditambahkan",
|
||||
"Successfully deleted": "Berhasil dihapus",
|
||||
"Successfully saved": "Berhasil disimpan",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "Metode HTTP",
|
||||
"New Webhook": "Webhook Baru",
|
||||
"Value": "Nilai"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "Peran yang termasuk dalam rencana saat ini",
|
||||
"PricePerMonth": "Harga per bulan",
|
||||
"PricePerYear": "Harga per tahun",
|
||||
"PerMonth": "per bulan"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "Rencana Tambahan",
|
||||
"Sub plans - Tooltip": "Rencana yang termasuk dalam harga saat ini",
|
||||
"Has trial": "Mempunyai periode percobaan",
|
||||
"Has trial - Tooltip": "Ketersediaan periode percobaan setelah memilih rencana",
|
||||
"Trial duration": "Durasi percobaan",
|
||||
"Trial duration - Tooltip": "Durasi periode percobaan",
|
||||
"Getting started": "Mulai",
|
||||
"Copy pricing page URL": "Salin URL halaman harga",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL halaman harga berhasil disalin ke clipboard, silakan tempelkan ke dalam jendela mode penyamaran atau browser lainnya",
|
||||
"days trial available!": "hari percobaan tersedia!",
|
||||
"Free": "Gratis"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "Durasi",
|
||||
"Duration - Tooltip": "Durasi langganan",
|
||||
"Start Date": "Tanggal Mulai",
|
||||
"Start Date - Tooltip": "Tanggal Mulai",
|
||||
"End Date": "Tanggal Berakhir",
|
||||
"End Date - Tooltip": "Tanggal Berakhir",
|
||||
"Sub users": "Pengguna Langganan",
|
||||
"Sub users - Tooltip": "Pengguna Langganan",
|
||||
"Sub plan": "Rencana Langganan",
|
||||
"Sub plan - Tooltip": "Rencana Langganan"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "電話",
|
||||
"Phone - Tooltip": "電話番号",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "プラン",
|
||||
"Pricings": "価格設定",
|
||||
"Preview": "プレビュー",
|
||||
"Preview - Tooltip": "構成されたエフェクトをプレビューする",
|
||||
"Products": "製品",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "申し訳ありませんが、このページにアクセスする権限がありません、またはログイン状態が無効です。",
|
||||
"State": "州",
|
||||
"State - Tooltip": "状態",
|
||||
"Subscriptions": "サブスクリプション",
|
||||
"Successfully added": "正常に追加されました",
|
||||
"Successfully deleted": "正常に削除されました",
|
||||
"Successfully saved": "成功的に保存されました",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "HTTPメソッド",
|
||||
"New Webhook": "新しいWebhook",
|
||||
"Value": "値"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "現在のプランに含まれるロール",
|
||||
"PricePerMonth": "月額料金",
|
||||
"PricePerYear": "年間料金",
|
||||
"PerMonth": "月毎"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "追加プラン",
|
||||
"Sub plans - Tooltip": "現在の価格設定に含まれるプラン",
|
||||
"Has trial": "トライアル期間あり",
|
||||
"Has trial - Tooltip": "プラン選択後のトライアル期間の有無",
|
||||
"Trial duration": "トライアル期間の長さ",
|
||||
"Trial duration - Tooltip": "トライアル期間の長さ",
|
||||
"Getting started": "はじめる",
|
||||
"Copy pricing page URL": "価格ページのURLをコピー",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "価格ページのURLが正常にクリップボードにコピーされました。シークレットウィンドウや別のブラウザに貼り付けてください。",
|
||||
"days trial available!": "日間のトライアルが利用可能です!",
|
||||
"Free": "無料"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "期間",
|
||||
"Duration - Tooltip": "購読の期間",
|
||||
"Start Date": "開始日",
|
||||
"Start Date - Tooltip": "開始日",
|
||||
"End Date": "終了日",
|
||||
"End Date - Tooltip": "終了日",
|
||||
"Sub users": "購読ユーザー",
|
||||
"Sub users - Tooltip": "購読ユーザー",
|
||||
"Sub plan": "購読プラン",
|
||||
"Sub plan - Tooltip": "購読プラン"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "전화기",
|
||||
"Phone - Tooltip": "전화 번호",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "플랜",
|
||||
"Pricings": "가격",
|
||||
"Preview": "미리보기",
|
||||
"Preview - Tooltip": "구성된 효과를 미리보기합니다",
|
||||
"Products": "제품들",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "죄송합니다. 이 페이지에 접근할 권한이 없거나 로그인 상태가 유효하지 않습니다.",
|
||||
"State": "주",
|
||||
"State - Tooltip": "국가",
|
||||
"Subscriptions": "구독",
|
||||
"Successfully added": "성공적으로 추가되었습니다",
|
||||
"Successfully deleted": "성공적으로 삭제되었습니다",
|
||||
"Successfully saved": "성공적으로 저장되었습니다",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "HTTP 방법",
|
||||
"New Webhook": "새로운 웹훅",
|
||||
"Value": "가치"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "현재 플랜에 포함된 역할",
|
||||
"PricePerMonth": "월별 가격",
|
||||
"PricePerYear": "연간 가격",
|
||||
"PerMonth": "월"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "추가 플랜",
|
||||
"Sub plans - Tooltip": "현재 가격 책정에 포함된 플랜",
|
||||
"Has trial": "무료 체험 가능",
|
||||
"Has trial - Tooltip": "플랜 선택 후 체험 기간의 가용 여부",
|
||||
"Trial duration": "체험 기간",
|
||||
"Trial duration - Tooltip": "체험 기간의 기간",
|
||||
"Getting started": "시작하기",
|
||||
"Copy pricing page URL": "가격 페이지 URL 복사",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "가격 페이지 URL이 클립보드에 성공적으로 복사되었습니다. 시크릿 창이나 다른 브라우저에 붙여넣기해주세요.",
|
||||
"days trial available!": "일 무료 체험 가능!",
|
||||
"Free": "무료"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "기간",
|
||||
"Duration - Tooltip": "구독 기간",
|
||||
"Start Date": "시작일",
|
||||
"Start Date - Tooltip": "시작일",
|
||||
"End Date": "종료일",
|
||||
"End Date - Tooltip": "종료일",
|
||||
"Sub users": "구독 사용자",
|
||||
"Sub users - Tooltip": "구독 사용자",
|
||||
"Sub plan": "구독 플랜",
|
||||
"Sub plan - Tooltip": "구독 플랜"
|
||||
}
|
||||
}
|
||||
|
889
web/src/locales/pt/data.json
Normal file
889
web/src/locales/pt/data.json
Normal file
@ -0,0 +1,889 @@
|
||||
{
|
||||
"account": {
|
||||
"Chats & Messages": "Conversas e Mensagens",
|
||||
"Logout": "Sair",
|
||||
"My Account": "Minha Conta",
|
||||
"Sign Up": "Cadastrar-se"
|
||||
},
|
||||
"adapter": {
|
||||
"Duplicated policy rules": "Regras de política duplicadas",
|
||||
"Edit Adapter": "Editar Adaptador",
|
||||
"Failed to sync policies": "Falha ao sincronizar as políticas",
|
||||
"New Adapter": "Novo Adaptador",
|
||||
"Policies": "Políticas",
|
||||
"Policies - Tooltip": "Regras de política do Casbin",
|
||||
"Sync policies successfully": "Políticas sincronizadas com sucesso"
|
||||
},
|
||||
"application": {
|
||||
"Always": "Sempre",
|
||||
"Auto signin": "Login automático",
|
||||
"Auto signin - Tooltip": "Quando uma sessão logada existe no Casdoor, ela é automaticamente usada para o login no lado da aplicação",
|
||||
"Background URL": "URL de Fundo",
|
||||
"Background URL - Tooltip": "URL da imagem de fundo usada na página de login",
|
||||
"Center": "Centro",
|
||||
"Copy SAML metadata URL": "Copiar URL de metadados SAML",
|
||||
"Copy prompt page URL": "Copiar URL da página de prompt",
|
||||
"Copy signin page URL": "Copiar URL da página de login",
|
||||
"Copy signup page URL": "Copiar URL da página de registro",
|
||||
"Dynamic": "Dinâmico",
|
||||
"Edit Application": "Editar Aplicação",
|
||||
"Enable Email linking": "Ativar vinculação de e-mail",
|
||||
"Enable Email linking - Tooltip": "Ao usar provedores de terceiros para fazer login, se houver um usuário na organização com o mesmo e-mail, o método de login de terceiros será automaticamente associado a esse usuário",
|
||||
"Enable SAML compression": "Ativar compressão SAML",
|
||||
"Enable SAML compression - Tooltip": "Se deve comprimir as mensagens de resposta SAML quando o Casdoor é usado como provedor de identidade SAML",
|
||||
"Enable WebAuthn signin": "Ativar login WebAuthn",
|
||||
"Enable WebAuthn signin - Tooltip": "Se permite que os usuários façam login com WebAuthn",
|
||||
"Enable code signin": "Ativar login com código",
|
||||
"Enable code signin - Tooltip": "Se permite que os usuários façam login com código de verificação de telefone ou e-mail",
|
||||
"Enable password": "Ativar senha",
|
||||
"Enable password - Tooltip": "Se permite que os usuários façam login com senha",
|
||||
"Enable side panel": "Ativar painel lateral",
|
||||
"Enable signin session - Tooltip": "Se o Casdoor mantém uma sessão depois de fazer login no Casdoor a partir da aplicação",
|
||||
"Enable signup": "Ativar registro",
|
||||
"Enable signup - Tooltip": "Se permite que os usuários registrem uma nova conta",
|
||||
"Failed to sign in": "Falha ao fazer login",
|
||||
"File uploaded successfully": "Arquivo enviado com sucesso",
|
||||
"First, last": "Primeiro, último",
|
||||
"Follow organization theme": "Seguir tema da organização",
|
||||
"Form CSS": "CSS do formulário",
|
||||
"Form CSS - Edit": "Editar CSS do formulário",
|
||||
"Form CSS - Tooltip": "Estilização CSS dos formulários de registro, login e recuperação de senha (por exemplo, adicionando bordas e sombras)",
|
||||
"Form CSS Mobile": "CSS do formulário em dispositivos móveis",
|
||||
"Form CSS Mobile - Edit": "Editar CSS do formulário em dispositivos móveis",
|
||||
"Form CSS Mobile - Tooltip": "CSS do formulário em dispositivos móveis - Dica",
|
||||
"Form position": "Posição do formulário",
|
||||
"Form position - Tooltip": "Localização dos formulários de registro, login e recuperação de senha",
|
||||
"Grant types": "Tipos de concessão",
|
||||
"Grant types - Tooltip": "Selecione quais tipos de concessão são permitidos no protocolo OAuth",
|
||||
"Incremental": "Incremental",
|
||||
"Left": "Esquerda",
|
||||
"Logged in successfully": "Login realizado com sucesso",
|
||||
"Logged out successfully": "Logout realizado com sucesso",
|
||||
"New Application": "Nova Aplicação",
|
||||
"No verification": "Sem verificação",
|
||||
"None": "Nenhum",
|
||||
"Normal": "Normal",
|
||||
"Only signup": "Apenas registro",
|
||||
"Please input your application!": "Por favor, insira o nome da sua aplicação!",
|
||||
"Please input your organization!": "Por favor, insira o nome da sua organização!",
|
||||
"Please select a HTML file": "Por favor, selecione um arquivo HTML",
|
||||
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL da página de prompt copiada para a área de transferência com sucesso. Cole-a na janela anônima ou em outro navegador",
|
||||
"Random": "Aleatório",
|
||||
"Real name": "Nome real",
|
||||
"Redirect URL": "URL de redirecionamento",
|
||||
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "URL de redirecionamento (URL de ligação de postagem de serviço do consumidor de afirmação)",
|
||||
"Redirect URLs": "URLs de redirecionamento",
|
||||
"Redirect URLs - Tooltip": "Lista de URLs de redirecionamento permitidos, com suporte à correspondência por expressões regulares; URLs que não estão na lista falharão ao redirecionar",
|
||||
"Refresh token expire": "Expiração do token de atualização",
|
||||
"Refresh token expire - Tooltip": "Tempo de expiração do token de atualização",
|
||||
"Right": "Direita",
|
||||
"Rule": "Regra",
|
||||
"SAML metadata": "Metadados do SAML",
|
||||
"SAML metadata - Tooltip": "Os metadados do protocolo SAML",
|
||||
"SAML metadata URL copied to clipboard successfully": "URL dos metadados do SAML copiada para a área de transferência com sucesso",
|
||||
"SAML reply URL": "URL de resposta do SAML",
|
||||
"Side panel HTML": "HTML do painel lateral",
|
||||
"Side panel HTML - Edit": "Editar HTML do painel lateral",
|
||||
"Side panel HTML - Tooltip": "Personalize o código HTML para o painel lateral da página de login",
|
||||
"Sign Up Error": "Erro ao Registrar",
|
||||
"Signin": "Login",
|
||||
"Signin (Default True)": "Login (Padrão Verdadeiro)",
|
||||
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL da página de login copiada para a área de transferência com sucesso. Cole-a na janela anônima ou em outro navegador",
|
||||
"Signin session": "Sessão de login",
|
||||
"Signup items": "Itens de registro",
|
||||
"Signup items - Tooltip": "Itens para os usuários preencherem ao registrar novas contas",
|
||||
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL da página de registro copiada para a área de transferência com sucesso. Cole-a na janela anônima ou em outro navegador",
|
||||
"The application does not allow to sign up new account": "A aplicação não permite o registro de novas contas",
|
||||
"Token expire": "Expiração do Token",
|
||||
"Token expire - Tooltip": "Tempo de expiração do token de acesso",
|
||||
"Token format": "Formato do token",
|
||||
"Token format - Tooltip": "O formato do token de acesso",
|
||||
"You are unexpected to see this prompt page": "Você não deveria ver esta página de prompt"
|
||||
},
|
||||
"cert": {
|
||||
"Bit size": "Tamanho do bit",
|
||||
"Bit size - Tooltip": "Comprimento da chave secreta",
|
||||
"Certificate": "Certificado",
|
||||
"Certificate - Tooltip": "Certificado de chave pública, usado para descriptografar a assinatura JWT do Token de Acesso. Este certificado geralmente precisa ser implantado no lado do SDK do Casdoor (ou seja, no aplicativo) para analisar o JWT",
|
||||
"Certificate copied to clipboard successfully": "Certificado copiado para a área de transferência com sucesso",
|
||||
"Copy certificate": "Copiar certificado",
|
||||
"Copy private key": "Copiar chave privada",
|
||||
"Crypto algorithm": "Algoritmo criptográfico",
|
||||
"Crypto algorithm - Tooltip": "Algoritmo de criptografia usado pelo certificado",
|
||||
"Download certificate": "Baixar certificado",
|
||||
"Download private key": "Baixar chave privada",
|
||||
"Edit Cert": "Editar Certificado",
|
||||
"Expire in years": "Expirar em anos",
|
||||
"Expire in years - Tooltip": "Período de validade do certificado, em anos",
|
||||
"New Cert": "Novo Certificado",
|
||||
"Private key": "Chave privada",
|
||||
"Private key - Tooltip": "Chave privada correspondente ao certificado de chave pública",
|
||||
"Private key copied to clipboard successfully": "Chave privada copiada para a área de transferência com sucesso",
|
||||
"Scope - Tooltip": "Cenários de uso do certificado",
|
||||
"Type - Tooltip": "Tipo de certificado"
|
||||
},
|
||||
"chat": {
|
||||
"AI": "IA",
|
||||
"Edit Chat": "Editar Chat",
|
||||
"Group": "Grupo",
|
||||
"Message count": "Contagem de Mensagens",
|
||||
"New Chat": "Novo Chat",
|
||||
"Single": "Individual",
|
||||
"User1": "Usuário 1",
|
||||
"User1 - Tooltip": "Usuário 1 - Tooltip",
|
||||
"User2": "Usuário 2",
|
||||
"User2 - Tooltip": "Usuário 2 - Tooltip",
|
||||
"Users - Tooltip": "Usuários - Tooltip"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Código que você recebeu",
|
||||
"Email code": "Código de e-mail",
|
||||
"Empty code": "Código vazio",
|
||||
"Enter your code": "Digite seu código",
|
||||
"Phone code": "Código de telefone",
|
||||
"Please input your phone verification code!": "Por favor, insira o código de verificação do telefone!",
|
||||
"Please input your verification code!": "Por favor, insira o seu código de verificação!",
|
||||
"Send Code": "Enviar Código",
|
||||
"Sending": "Enviando",
|
||||
"Submit and complete": "Enviar e concluir"
|
||||
},
|
||||
"forget": {
|
||||
"Account": "Conta",
|
||||
"Change Password": "Alterar Senha",
|
||||
"Choose email or phone": "Escolha e-mail ou telefone",
|
||||
"Next Step": "Próxima Etapa",
|
||||
"Please input your username!": "Por favor, insira seu nome de usuário!",
|
||||
"Reset": "Redefinir",
|
||||
"Retrieve password": "Recuperar senha",
|
||||
"Unknown forget type": "Tipo de recuperação desconhecido",
|
||||
"Verify": "Verificar"
|
||||
},
|
||||
"general": {
|
||||
"Action": "Ação",
|
||||
"Adapter": "Adaptador",
|
||||
"Adapter - Tooltip": "Nome da tabela do armazenamento de políticas",
|
||||
"Adapters": "Adaptadores",
|
||||
"Add": "Adicionar",
|
||||
"Affiliation URL": "URL da Afiliação",
|
||||
"Affiliation URL - Tooltip": "A URL da página inicial para a afiliação",
|
||||
"Application": "Aplicação",
|
||||
"Applications": "Aplicações",
|
||||
"Applications that require authentication": "Aplicações que requerem autenticação",
|
||||
"Avatar": "Avatar",
|
||||
"Avatar - Tooltip": "Imagem de avatar pública do usuário",
|
||||
"Back": "Voltar",
|
||||
"Back Home": "Voltar para a Página Inicial",
|
||||
"Cancel": "Cancelar",
|
||||
"Captcha": "Captcha",
|
||||
"Cert": "Certificado",
|
||||
"Cert - Tooltip": "O certificado da chave pública que precisa ser verificado pelo SDK do cliente correspondente a esta aplicação",
|
||||
"Certs": "Certificados",
|
||||
"Chats": "Chats",
|
||||
"Click to Upload": "Clique para Enviar",
|
||||
"Client IP": "IP do Cliente",
|
||||
"Close": "Fechar",
|
||||
"Created time": "Hora de Criação",
|
||||
"Default application": "Aplicação padrão",
|
||||
"Default application - Tooltip": "Aplicação padrão para usuários registrados diretamente na página da organização",
|
||||
"Default avatar": "Avatar padrão",
|
||||
"Default avatar - Tooltip": "Avatar padrão usado quando usuários recém-registrados não definem uma imagem de avatar",
|
||||
"Delete": "Excluir",
|
||||
"Description": "Descrição",
|
||||
"Description - Tooltip": "Informações de descrição detalhadas para referência, o Casdoor em si não irá utilizá-las",
|
||||
"Display name": "Nome de exibição",
|
||||
"Display name - Tooltip": "Um nome amigável e facilmente legível exibido publicamente na interface do usuário",
|
||||
"Down": "Descer",
|
||||
"Edit": "Editar",
|
||||
"Email": "E-mail",
|
||||
"Email - Tooltip": "Endereço de e-mail válido",
|
||||
"Enable": "Habilitar",
|
||||
"Enabled": "Habilitado",
|
||||
"Enabled successfully": "Habilitado com sucesso",
|
||||
"Failed to add": "Falha ao adicionar",
|
||||
"Failed to connect to server": "Falha ao conectar ao servidor",
|
||||
"Failed to delete": "Falha ao excluir",
|
||||
"Failed to enable": "Falha ao habilitar",
|
||||
"Failed to get answer": "Falha ao obter resposta",
|
||||
"Failed to save": "Falha ao salvar",
|
||||
"Failed to verify": "Falha ao verificar",
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "URL do ícone de favicon usado em todas as páginas do Casdoor da organização",
|
||||
"First name": "Nome",
|
||||
"Forget URL": "URL de Esqueci a Senha",
|
||||
"Forget URL - Tooltip": "URL personalizada para a página de \"Esqueci a senha\". Se não definido, será usada a página padrão de \"Esqueci a senha\" do Casdoor. Quando definido, o link de \"Esqueci a senha\" na página de login será redirecionado para esta URL",
|
||||
"Found some texts still not translated? Please help us translate at": "Encontrou algum texto ainda não traduzido? Ajude-nos a traduzir em",
|
||||
"Go to writable demo site?": "Acessar o site de demonstração gravável?",
|
||||
"Home": "Página Inicial",
|
||||
"Home - Tooltip": "Página inicial do aplicativo",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "String única aleatória",
|
||||
"Is enabled": "Está habilitado",
|
||||
"Is enabled - Tooltip": "Define se está habilitado",
|
||||
"LDAPs": "LDAPs",
|
||||
"LDAPs - Tooltip": "Servidores LDAP",
|
||||
"Languages": "Idiomas",
|
||||
"Languages - Tooltip": "Idiomas disponíveis",
|
||||
"Last name": "Sobrenome",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Ícones que o aplicativo apresenta para o mundo externo",
|
||||
"Master password": "Senha mestra",
|
||||
"Master password - Tooltip": "Pode ser usada para fazer login em todos os usuários desta organização, facilitando para os administradores fazerem login como este usuário para resolver problemas técnicos",
|
||||
"Menu": "Menu",
|
||||
"Messages": "Mensagens",
|
||||
"Method": "Método",
|
||||
"Model": "Modelo",
|
||||
"Model - Tooltip": "Modelo de controle de acesso do Casbin",
|
||||
"Models": "Modelos",
|
||||
"Name": "Nome",
|
||||
"Name - Tooltip": "ID único em formato de string",
|
||||
"OAuth providers": "Provedores OAuth",
|
||||
"OK": "OK",
|
||||
"Organization": "Organização",
|
||||
"Organization - Tooltip": "Semelhante a conceitos como inquilinos ou grupos de usuários, cada usuário e aplicativo pertence a uma organização",
|
||||
"Organizations": "Organizações",
|
||||
"Password": "Senha",
|
||||
"Password - Tooltip": "Certifique-se de que a senha está correta",
|
||||
"Password salt": "Salt de senha",
|
||||
"Password salt - Tooltip": "Parâmetro aleatório usado para criptografia de senha",
|
||||
"Password type": "Tipo de senha",
|
||||
"Password type - Tooltip": "Formato de armazenamento de senhas no banco de dados",
|
||||
"Payments": "Pagamentos",
|
||||
"Permissions": "Permissões",
|
||||
"Permissions - Tooltip": "Permissões pertencentes a este usuário",
|
||||
"Phone": "Telefone",
|
||||
"Phone - Tooltip": "Número de telefone",
|
||||
"Phone or email": "Telefone ou email",
|
||||
"Preview": "Visualizar",
|
||||
"Preview - Tooltip": "Visualizar os efeitos configurados",
|
||||
"Products": "Produtos",
|
||||
"Provider": "Provedor",
|
||||
"Provider - Tooltip": "Provedores de pagamento a serem configurados, incluindo PayPal, Alipay, WeChat Pay, etc.",
|
||||
"Providers": "Provedores",
|
||||
"Providers - Tooltip": "Provedores a serem configurados, incluindo login de terceiros, armazenamento de objetos, código de verificação, etc.",
|
||||
"Real name": "Nome real",
|
||||
"Records": "Registros",
|
||||
"Request URI": "URI da solicitação",
|
||||
"Resources": "Recursos",
|
||||
"Roles": "Funções",
|
||||
"Roles - Tooltip": "Funções às quais o usuário pertence",
|
||||
"Save": "Salvar",
|
||||
"Save & Exit": "Salvar e Sair",
|
||||
"Session ID": "ID da sessão",
|
||||
"Sessions": "Sessões",
|
||||
"Signin URL": "URL de login",
|
||||
"Signin URL - Tooltip": "URL personalizada para a página de login. Se não definido, será usada a página padrão de login do Casdoor. Quando definido, os links de login em várias páginas do Casdoor serão redirecionados para esta URL",
|
||||
"Signup URL": "URL de registro",
|
||||
"Signup URL - Tooltip": "URL personalizada para a página de registro. Se não definido, será usada a página padrão de registro do Casdoor. Quando definido, os links de registro em várias páginas do Casdoor serão redirecionados para esta URL",
|
||||
"Signup application": "Aplicativo de registro",
|
||||
"Signup application - Tooltip": "Qual aplicativo o usuário usou para se registrar quando se inscreveu",
|
||||
"Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Desculpe, o usuário que você visitou não existe ou você não está autorizado a acessar este usuário.",
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Desculpe, você não tem permissão para acessar esta página ou o status de login é inválido.",
|
||||
"State": "Estado",
|
||||
"State - Tooltip": "Estado",
|
||||
"Successfully added": "Adicionado com sucesso",
|
||||
"Successfully deleted": "Excluído com sucesso",
|
||||
"Successfully saved": "Salvo com sucesso",
|
||||
"Supported country codes": "Códigos de país suportados",
|
||||
"Supported country codes - Tooltip": "Códigos de país suportados pela organização. Esses códigos podem ser selecionados como prefixo ao enviar códigos de verificação SMS",
|
||||
"Sure to delete": "Tem certeza que deseja excluir",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sincronizar",
|
||||
"Syncers": "Sincronizadores",
|
||||
"System Info": "Informações do Sistema",
|
||||
"There was a problem signing you in..": "Ocorreu um problema ao fazer o login.",
|
||||
"This is a read-only demo site!": "Este é um site de demonstração apenas para leitura!",
|
||||
"Timestamp": "Carimbo de Data/Hora",
|
||||
"Tokens": "Tokens",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "Link da URL",
|
||||
"Up": "Acima",
|
||||
"Updated time": "Hora de Atualização",
|
||||
"User": "Usuário",
|
||||
"User - Tooltip": "Certifique-se de que o nome de usuário esteja correto",
|
||||
"User containers": "Pools de Usuários",
|
||||
"User type": "Tipo de Usuário",
|
||||
"User type - Tooltip": "Tags às quais o usuário pertence, com valor padrão de \"usuário-normal\"",
|
||||
"Users": "Usuários",
|
||||
"Users under all organizations": "Usuários em todas as organizações",
|
||||
"Webhooks": "Webhooks",
|
||||
"empty": "vazio",
|
||||
"{total} in total": "{total} no total"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Administrador",
|
||||
"Admin - Tooltip": "CN ou ID do administrador do servidor LDAP",
|
||||
"Admin Password": "Senha do Administrador",
|
||||
"Admin Password - Tooltip": "Senha do administrador do servidor LDAP",
|
||||
"Auto Sync": "Sincronização Automática",
|
||||
"Auto Sync - Tooltip": "Configuração de sincronização automática, desativada em 0",
|
||||
"Base DN": "Base DN",
|
||||
"Base DN - Tooltip": "Base DN durante a busca LDAP",
|
||||
"CN": "CN",
|
||||
"Edit LDAP": "Editar LDAP",
|
||||
"Enable SSL": "Habilitar SSL",
|
||||
"Enable SSL - Tooltip": "Se habilitar o SSL",
|
||||
"Filter fields": "Campos de Filtro",
|
||||
"Filter fields - Tooltip": "Campos de filtro - Tooltip",
|
||||
"Group ID": "ID do Grupo",
|
||||
"Last Sync": "Última Sincronização",
|
||||
"Search Filter": "Filtro de Busca",
|
||||
"Search Filter - Tooltip": "Filtro de busca - Tooltip",
|
||||
"Server": "Servidor",
|
||||
"Server host": "Host do Servidor",
|
||||
"Server host - Tooltip": "Endereço do servidor LDAP",
|
||||
"Server name": "Nome do Servidor",
|
||||
"Server name - Tooltip": "Nome de exibição da configuração do servidor LDAP",
|
||||
"Server port": "Porta do Servidor",
|
||||
"Server port - Tooltip": "Porta do servidor LDAP",
|
||||
"The Auto Sync option will sync all users to specify organization": "A opção de Sincronização Automática irá sincronizar todos os usuários para a organização especificada",
|
||||
"synced": "Sincronizado",
|
||||
"unsynced": "Não sincronizado"
|
||||
},
|
||||
"login": {
|
||||
"Auto sign in": "Entrar automaticamente",
|
||||
"Continue with": "Continuar com",
|
||||
"Email or phone": "Email ou telefone",
|
||||
"Forgot password?": "Esqueceu a senha?",
|
||||
"Loading": "Carregando",
|
||||
"Logging out...": "Saindo...",
|
||||
"No account?": "Não possui uma conta?",
|
||||
"Or sign in with another account": "Ou entre com outra conta",
|
||||
"Please input your Email or Phone!": "Por favor, informe seu email ou telefone!",
|
||||
"Please input your code!": "Por favor, informe o código!",
|
||||
"Please input your password!": "Por favor, informe sua senha!",
|
||||
"Please input your password, at least 6 characters!": "Por favor, informe sua senha, pelo menos 6 caracteres!",
|
||||
"Redirecting, please wait.": "Redirecionando, por favor aguarde.",
|
||||
"Sign In": "Entrar",
|
||||
"Sign in with WebAuthn": "Entrar com WebAuthn",
|
||||
"Sign in with {type}": "Entrar com {type}",
|
||||
"Signing in...": "Entrando...",
|
||||
"Successfully logged in with WebAuthn credentials": "Logado com sucesso usando credenciais WebAuthn",
|
||||
"The input is not valid Email or phone number!": "O valor inserido não é um email ou número de telefone válido!",
|
||||
"To access": "Para acessar",
|
||||
"Verification code": "Código de verificação",
|
||||
"WebAuthn": "WebAuthn",
|
||||
"sign up now": "Inscreva-se agora",
|
||||
"username, Email or phone": "Nome de usuário, email ou telefone"
|
||||
},
|
||||
"message": {
|
||||
"Author": "Autor",
|
||||
"Author - Tooltip": "Autor - Dica de ferramenta",
|
||||
"Chat": "Chat",
|
||||
"Chat - Tooltip": "Chat - Dica de ferramenta",
|
||||
"Edit Message": "Editar Mensagem",
|
||||
"New Message": "Nova Mensagem",
|
||||
"Text": "Texto",
|
||||
"Text - Tooltip": "Texto - Dica de ferramenta"
|
||||
},
|
||||
"mfa": {
|
||||
"Each time you sign in to your Account, you'll need your password and a authentication code": "Cada vez que você entrar na sua Conta, você precisará da sua senha e de um código de autenticação",
|
||||
"Failed to get application": "Falha ao obter o aplicativo",
|
||||
"Failed to initiate MFA": "Falha ao iniciar MFA",
|
||||
"Have problems?": "Está com problemas?",
|
||||
"Multi-factor authentication": "Autenticação de vários fatores",
|
||||
"Multi-factor authentication - Tooltip ": "Autenticação de vários fatores - Dica de ferramenta",
|
||||
"Multi-factor authentication description": "Configurar autenticação de vários fatores",
|
||||
"Multi-factor methods": "Métodos de vários fatores",
|
||||
"Multi-factor recover": "Recuperação de vários fatores",
|
||||
"Multi-factor recover description": "Se você não conseguir acessar seu dispositivo, insira seu código de recuperação para verificar sua identidade",
|
||||
"Multi-factor secret": "Segredo de vários fatores",
|
||||
"Multi-factor secret - Tooltip": "Segredo de vários fatores - Dica de ferramenta",
|
||||
"Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso",
|
||||
"Passcode": "Código de acesso",
|
||||
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação",
|
||||
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores",
|
||||
"Recovery code": "Código de recuperação",
|
||||
"SMS/Email message": "Mensagem SMS/E-mail",
|
||||
"Set preferred": "Definir preferido",
|
||||
"Setup": "Configuração",
|
||||
"Use SMS verification code": "Usar código de verificação SMS",
|
||||
"Use a recovery code": "Usar um código de recuperação",
|
||||
"Verification failed": "Verificação falhou",
|
||||
"Verify Code": "Verificar Código",
|
||||
"Verify Password": "Verificar Senha",
|
||||
"Your email is": "Seu e-mail é",
|
||||
"Your phone is": "Seu telefone é",
|
||||
"preferred": "Preferido"
|
||||
},
|
||||
"model": {
|
||||
"Edit Model": "Editar Modelo",
|
||||
"Model text": "Texto do Modelo",
|
||||
"Model text - Tooltip": "Modelo de controle de acesso Casbin, incluindo modelos incorporados como ACL, RBAC, ABAC, RESTful, etc. Você também pode criar modelos personalizados. Para obter mais informações, visite o site do Casbin",
|
||||
"New Model": "Novo Modelo"
|
||||
},
|
||||
"organization": {
|
||||
"Account items": "Itens da Conta",
|
||||
"Account items - Tooltip": "Itens na página de Configurações Pessoais",
|
||||
"Edit Organization": "Editar Organização",
|
||||
"Follow global theme": "Seguir tema global",
|
||||
"Init score": "Pontuação inicial",
|
||||
"Init score - Tooltip": "Pontos de pontuação inicial concedidos aos usuários no momento do registro",
|
||||
"Is profile public": "Perfil é público",
|
||||
"Is profile public - Tooltip": "Após ser fechado, apenas administradores globais ou usuários na mesma organização podem acessar a página de perfil do usuário",
|
||||
"Modify rule": "Modificar regra",
|
||||
"New Organization": "Nova Organização",
|
||||
"Soft deletion": "Exclusão suave",
|
||||
"Soft deletion - Tooltip": "Quando ativada, a exclusão de usuários não os removerá completamente do banco de dados. Em vez disso, eles serão marcados como excluídos",
|
||||
"Tags": "Tags",
|
||||
"Tags - Tooltip": "Coleção de tags disponíveis para os usuários escolherem",
|
||||
"View rule": "Ver regra",
|
||||
"Visible": "Visível",
|
||||
"Website URL": "URL do website",
|
||||
"Website URL - Tooltip": "A URL da página inicial da organização. Este campo não é utilizado no Casdoor"
|
||||
},
|
||||
"payment": {
|
||||
"Confirm your invoice information": "Confirme as informações da sua fatura",
|
||||
"Currency": "Moeda",
|
||||
"Currency - Tooltip": "Como USD, CNY, etc.",
|
||||
"Download Invoice": "Baixar Fatura",
|
||||
"Edit Payment": "Editar Pagamento",
|
||||
"Individual": "Individual",
|
||||
"Invoice URL": "URL da Fatura",
|
||||
"Invoice URL - Tooltip": "URL para baixar a fatura",
|
||||
"Invoice actions": "Ações da Fatura",
|
||||
"Invoice actions - Tooltip": "Operações incluem emissão de faturas e download de faturas",
|
||||
"Invoice remark": "Observação da Fatura",
|
||||
"Invoice remark - Tooltip": "A observação não deve exceder 50 caracteres",
|
||||
"Invoice tax ID": "ID Fiscal da Fatura",
|
||||
"Invoice tax ID - Tooltip": "Quando o tipo de fatura é para uma organização, é necessário informar o número de identificação fiscal da organização; quando o tipo de fatura é para uma pessoa física, não é necessário preencher esta informação",
|
||||
"Invoice title": "Título da Fatura",
|
||||
"Invoice title - Tooltip": "Quando o tipo de fatura é para uma organização, pode ser inserido o nome da organização como título da fatura; quando o tipo de fatura é para uma pessoa física, o sistema preencherá automaticamente o nome do pagador",
|
||||
"Invoice type": "Tipo de Fatura",
|
||||
"Invoice type - Tooltip": "O tipo de fatura pode ser para uma pessoa física ou uma organização",
|
||||
"Issue Invoice": "Emitir Fatura",
|
||||
"Message": "Mensagem",
|
||||
"Message - Tooltip": "Mensagem de resultado do processamento do pagamento",
|
||||
"New Payment": "Novo Pagamento",
|
||||
"Person Email": "Email da Pessoa",
|
||||
"Person Email - Tooltip": "Email do pagador",
|
||||
"Person ID card": "Documento de Identificação",
|
||||
"Person ID card - Tooltip": "Número do documento de identificação do pagador",
|
||||
"Person name": "Nome da Pessoa",
|
||||
"Person name - Tooltip": "Nome real do pagador",
|
||||
"Person phone": "Telefone da Pessoa",
|
||||
"Person phone - Tooltip": "Número de telefone do pagador",
|
||||
"Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.": "Por favor, verifique cuidadosamente as informações da sua fatura. Uma vez emitida, a fatura não pode ser cancelada ou modificada.",
|
||||
"Please click the below button to return to the original website": "Por favor, clique no botão abaixo para retornar ao site original",
|
||||
"Please pay the order first!": "Por favor, faça o pagamento do pedido primeiro!",
|
||||
"Processing...": "Processando...",
|
||||
"Product": "Produto",
|
||||
"Product - Tooltip": "Nome do Produto",
|
||||
"Result": "Resultado",
|
||||
"Return to Website": "Retornar ao Website",
|
||||
"The payment has failed": "O pagamento falhou",
|
||||
"The payment is still under processing": "O pagamento ainda está sendo processado",
|
||||
"Type - Tooltip": "Método de pagamento utilizado ao comprar o produto",
|
||||
"You have successfully completed the payment": "Você concluiu o pagamento com sucesso",
|
||||
"please wait for a few seconds...": "por favor, aguarde alguns segundos...",
|
||||
"the current state is": "o estado atual é"
|
||||
},
|
||||
"permission": {
|
||||
"Actions": "Ações",
|
||||
"Actions - Tooltip": "Ações permitidas",
|
||||
"Admin": "Administrador",
|
||||
"Allow": "Permitir",
|
||||
"Approve time": "Horário de Aprovação",
|
||||
"Approve time - Tooltip": "O horário de aprovação desta permissão",
|
||||
"Approved": "Aprovado",
|
||||
"Approver": "Aprovador",
|
||||
"Approver - Tooltip": "A pessoa que aprovou a permissão",
|
||||
"Deny": "Negar",
|
||||
"Edit Permission": "Editar Permissão",
|
||||
"Effect": "Efeito",
|
||||
"Effect - Tooltip": "Permitir ou rejeitar",
|
||||
"New Permission": "Nova Permissão",
|
||||
"Pending": "Pendente",
|
||||
"Read": "Ler",
|
||||
"Resource type": "Tipo de Recurso",
|
||||
"Resource type - Tooltip": "Tipo de recurso",
|
||||
"Resources - Tooltip": "Recursos autorizados",
|
||||
"Submitter": "Requerente",
|
||||
"Submitter - Tooltip": "A pessoa que está solicitando esta permissão",
|
||||
"TreeNode": "Nó da Árvore",
|
||||
"Write": "Escrever"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Comprar",
|
||||
"Buy Product": "Comprar Produto",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detalhe",
|
||||
"Detail - Tooltip": "Detalhes do produto",
|
||||
"Edit Product": "Editar Produto",
|
||||
"I have completed the payment": "Eu concluí o pagamento",
|
||||
"Image": "Imagem",
|
||||
"Image - Tooltip": "Imagem do produto",
|
||||
"New Product": "Novo Produto",
|
||||
"Pay": "Pagar",
|
||||
"PayPal": "PayPal",
|
||||
"Payment providers": "Provedores de Pagamento",
|
||||
"Payment providers - Tooltip": "Fornecedores de serviços de pagamento",
|
||||
"Placing order...": "Processando pedido...",
|
||||
"Please provide your username in the remark": "Por favor, forneça seu nome de usuário na observação",
|
||||
"Please scan the QR code to pay": "Por favor, escaneie o código QR para pagar",
|
||||
"Price": "Preço",
|
||||
"Price - Tooltip": "Preço do produto",
|
||||
"Quantity": "Quantidade",
|
||||
"Quantity - Tooltip": "Quantidade do produto",
|
||||
"Return URL": "URL de Retorno",
|
||||
"Return URL - Tooltip": "URL para retornar após a compra bem-sucedida",
|
||||
"SKU": "SKU",
|
||||
"Sold": "Vendido",
|
||||
"Sold - Tooltip": "Quantidade vendida",
|
||||
"Tag - Tooltip": "Tag do produto",
|
||||
"Test buy page..": "Página de teste de compra...",
|
||||
"There is no payment channel for this product.": "Não há canal de pagamento disponível para este produto.",
|
||||
"This product is currently not in sale.": "Este produto não está disponível para venda no momento.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
"Access key": "Chave de acesso",
|
||||
"Access key - Tooltip": "Chave de acesso",
|
||||
"Agent ID": "ID do Agente",
|
||||
"Agent ID - Tooltip": "ID do Agente",
|
||||
"App ID": "ID do aplicativo",
|
||||
"App ID - Tooltip": "ID do aplicativo",
|
||||
"App key": "Chave do aplicativo",
|
||||
"App key - Tooltip": "Chave do aplicativo",
|
||||
"App secret": "Segredo do aplicativo",
|
||||
"AppSecret - Tooltip": "Segredo do aplicativo",
|
||||
"Auth URL": "URL de autenticação",
|
||||
"Auth URL - Tooltip": "URL de autenticação",
|
||||
"Bucket": "Bucket",
|
||||
"Bucket - Tooltip": "Nome do bucket",
|
||||
"Can not parse metadata": "Não é possível analisar metadados",
|
||||
"Can signin": "Pode fazer login",
|
||||
"Can signup": "Pode se inscrever",
|
||||
"Can unlink": "Pode desvincular",
|
||||
"Category": "Categoria",
|
||||
"Category - Tooltip": "Selecione uma categoria",
|
||||
"Channel No.": "Número do canal",
|
||||
"Channel No. - Tooltip": "Número do canal",
|
||||
"Client ID": "ID do cliente",
|
||||
"Client ID - Tooltip": "ID do cliente",
|
||||
"Client ID 2": "ID do cliente 2",
|
||||
"Client ID 2 - Tooltip": "O segundo ID do cliente",
|
||||
"Client secret": "Segredo do cliente",
|
||||
"Client secret - Tooltip": "Segredo do cliente",
|
||||
"Client secret 2": "Segredo do cliente 2",
|
||||
"Client secret 2 - Tooltip": "A segunda chave secreta do cliente",
|
||||
"Copy": "Copiar",
|
||||
"Disable SSL": "Desabilitar SSL",
|
||||
"Disable SSL - Tooltip": "Se deve desabilitar o protocolo SSL ao comunicar com o servidor SMTP",
|
||||
"Domain": "Domínio",
|
||||
"Domain - Tooltip": "Domínio personalizado para armazenamento de objetos",
|
||||
"Edit Provider": "Editar Provedor",
|
||||
"Email content": "Conteúdo do e-mail",
|
||||
"Email content - Tooltip": "Conteúdo do e-mail",
|
||||
"Email sent successfully": "E-mail enviado com sucesso",
|
||||
"Email title": "Título do e-mail",
|
||||
"Email title - Tooltip": "Título do e-mail",
|
||||
"Enable QR code": "Habilitar código QR",
|
||||
"Enable QR code - Tooltip": "Se permite escanear código QR para fazer login",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Endpoint (Intranet)",
|
||||
"From address": "Endereço do remetente",
|
||||
"From address - Tooltip": "Endereço de e-mail do remetente",
|
||||
"From name": "Nome do remetente",
|
||||
"From name - Tooltip": "Nome do remetente",
|
||||
"Host": "Host",
|
||||
"Host - Tooltip": "Nome do host",
|
||||
"IdP": "IdP",
|
||||
"IdP certificate": "Certificado IdP",
|
||||
"Intelligent Validation": "Validação inteligente",
|
||||
"Internal": "Interno",
|
||||
"Issuer URL": "URL do Emissor",
|
||||
"Issuer URL - Tooltip": "URL do Emissor",
|
||||
"Link copied to clipboard successfully": "Link copiado para a área de transferência com sucesso",
|
||||
"Metadata": "Metadados",
|
||||
"Metadata - Tooltip": "Metadados SAML",
|
||||
"Method - Tooltip": "Método de login, código QR ou login silencioso",
|
||||
"New Provider": "Novo Provedor",
|
||||
"Normal": "Normal",
|
||||
"Parse": "Analisar",
|
||||
"Parse metadata successfully": "Metadados analisados com sucesso",
|
||||
"Path prefix": "Prefixo do caminho",
|
||||
"Path prefix - Tooltip": "Prefixo do caminho do bucket para armazenamento de objetos",
|
||||
"Please use WeChat and scan the QR code to sign in": "Por favor, use o WeChat e escaneie o código QR para fazer login",
|
||||
"Port": "Porta",
|
||||
"Port - Tooltip": "Certifique-se de que a porta esteja aberta",
|
||||
"Prompted": "Solicitado",
|
||||
"Provider URL": "URL do Provedor",
|
||||
"Provider URL - Tooltip": "URL para configurar o provedor de serviço, este campo é apenas usado para referência e não é usado no Casdoor",
|
||||
"Region ID": "ID da Região",
|
||||
"Region ID - Tooltip": "ID da região para o provedor de serviços",
|
||||
"Region endpoint for Internet": "Endpoint da região para a Internet",
|
||||
"Region endpoint for Intranet": "Endpoint da região para Intranet",
|
||||
"Required": "Obrigatório",
|
||||
"SAML 2.0 Endpoint (HTTP)": "Ponto de extremidade SAML 2.0 (HTTP)",
|
||||
"SMS Test": "Teste de SMS",
|
||||
"SMS Test - Tooltip": "Número de telefone para enviar SMS de teste",
|
||||
"SMS account": "Conta SMS",
|
||||
"SMS account - Tooltip": "Conta SMS",
|
||||
"SMS sent successfully": "SMS enviado com sucesso",
|
||||
"SP ACS URL": "URL SP ACS",
|
||||
"SP ACS URL - Tooltip": "URL SP ACS",
|
||||
"SP Entity ID": "ID da Entidade SP",
|
||||
"Scene": "Cenário",
|
||||
"Scene - Tooltip": "Cenário",
|
||||
"Scope": "Escopo",
|
||||
"Scope - Tooltip": "Escopo",
|
||||
"Secret access key": "Chave de acesso secreta",
|
||||
"Secret access key - Tooltip": "Chave de acesso secreta",
|
||||
"Secret key": "Chave secreta",
|
||||
"Secret key - Tooltip": "Usada pelo servidor para chamar a API do fornecedor de código de verificação para verificação",
|
||||
"Send Testing Email": "Enviar E-mail de Teste",
|
||||
"Send Testing SMS": "Enviar SMS de Teste",
|
||||
"Sign Name": "Nome do Sinal",
|
||||
"Sign Name - Tooltip": "Nome da assinatura a ser usada",
|
||||
"Sign request": "Solicitação de assinatura",
|
||||
"Sign request - Tooltip": "Se a solicitação requer uma assinatura",
|
||||
"Signin HTML": "HTML de login",
|
||||
"Signin HTML - Edit": "Editar HTML de login",
|
||||
"Signin HTML - Tooltip": "HTML personalizado para substituir o estilo padrão da página de login",
|
||||
"Signup HTML": "HTML de inscrição",
|
||||
"Signup HTML - Edit": "Editar HTML de inscrição",
|
||||
"Signup HTML - Tooltip": "HTML personalizado para substituir o estilo padrão da página de inscrição",
|
||||
"Silent": "Silencioso",
|
||||
"Site key": "Chave do site",
|
||||
"Site key - Tooltip": "Chave do site",
|
||||
"Sliding Validation": "Validação deslizante",
|
||||
"Sub type": "Subtipo",
|
||||
"Sub type - Tooltip": "Subtipo",
|
||||
"Template code": "Código do modelo",
|
||||
"Template code - Tooltip": "Código do modelo",
|
||||
"Test Email": "Testar E-mail",
|
||||
"Test Email - Tooltip": "Endereço de e-mail para receber e-mails de teste",
|
||||
"Test SMTP Connection": "Testar Conexão SMTP",
|
||||
"Third-party": "Terceiros",
|
||||
"Token URL": "URL do Token",
|
||||
"Token URL - Tooltip": "URL do Token",
|
||||
"Type": "Tipo",
|
||||
"Type - Tooltip": "Selecione um tipo",
|
||||
"UserInfo URL": "URL do UserInfo",
|
||||
"UserInfo URL - Tooltip": "URL do UserInfo",
|
||||
"admin (Shared)": "admin (Compartilhado)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Foi acionado"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copiar Link",
|
||||
"File name": "Nome do arquivo",
|
||||
"File size": "Tamanho do arquivo",
|
||||
"Format": "Formato",
|
||||
"Parent": "Pai",
|
||||
"Upload a file...": "Enviar um arquivo..."
|
||||
},
|
||||
"role": {
|
||||
"Edit Role": "Editar Função",
|
||||
"New Role": "Nova Função",
|
||||
"Sub domains": "Subdomínios",
|
||||
"Sub domains - Tooltip": "Domínios incluídos na função atual",
|
||||
"Sub roles": "Subfunções",
|
||||
"Sub roles - Tooltip": "Funções incluídas na função atual",
|
||||
"Sub users": "Subusuários",
|
||||
"Sub users - Tooltip": "Usuários incluídos na função atual"
|
||||
},
|
||||
"signup": {
|
||||
"Accept": "Aceitar",
|
||||
"Agreement": "Acordo",
|
||||
"Confirm": "Confirmar",
|
||||
"Decline": "Recusar",
|
||||
"Have account?": "Já possui uma conta?",
|
||||
"Please accept the agreement!": "Por favor, aceite o acordo!",
|
||||
"Please click the below button to sign in": "Por favor, clique no botão abaixo para fazer login",
|
||||
"Please confirm your password!": "Por favor, confirme sua senha!",
|
||||
"Please input the correct ID card number!": "Por favor, insira o número correto do seu cartão de identificação!",
|
||||
"Please input your Email!": "Por favor, insira seu Email!",
|
||||
"Please input your ID card number!": "Por favor, insira o número do seu cartão de identificação!",
|
||||
"Please input your address!": "Por favor, insira seu endereço!",
|
||||
"Please input your affiliation!": "Por favor, insira sua afiliação!",
|
||||
"Please input your display name!": "Por favor, insira seu nome de exibição!",
|
||||
"Please input your first name!": "Por favor, insira seu primeiro nome!",
|
||||
"Please input your last name!": "Por favor, insira seu sobrenome!",
|
||||
"Please input your phone number!": "Por favor, insira seu número de telefone!",
|
||||
"Please input your real name!": "Por favor, insira seu nome real!",
|
||||
"Please select your country code!": "Por favor, selecione o código do seu país!",
|
||||
"Please select your country/region!": "Por favor, selecione seu país/região!",
|
||||
"Terms of Use": "Termos de Uso",
|
||||
"Terms of Use - Tooltip": "Termos de uso que os usuários precisam ler e concordar durante o registro",
|
||||
"The input is not invoice Tax ID!": "A entrada não é um ID fiscal de fatura válido!",
|
||||
"The input is not invoice title!": "A entrada não é um título de fatura válido!",
|
||||
"The input is not valid Email!": "A entrada não é um Email válido!",
|
||||
"The input is not valid Phone!": "A entrada não é um número de telefone válido!",
|
||||
"Username": "Nome de usuário",
|
||||
"Username - Tooltip": "Nome de usuário - Tooltip",
|
||||
"Your account has been created!": "Sua conta foi criada!",
|
||||
"Your confirmed password is inconsistent with the password!": "Sua senha confirmada não é consistente com a senha!",
|
||||
"sign in now": "Faça login agora"
|
||||
},
|
||||
"syncer": {
|
||||
"Affiliation table": "Tabela de Afiliação",
|
||||
"Affiliation table - Tooltip": "Nome da tabela no banco de dados da unidade de trabalho",
|
||||
"Avatar base URL": "URL base do Avatar",
|
||||
"Avatar base URL - Tooltip": "Prefixo URL para as imagens de avatar",
|
||||
"Casdoor column": "Coluna Casdoor",
|
||||
"Column name": "Nome da coluna",
|
||||
"Column type": "Tipo de coluna",
|
||||
"Database": "Banco de dados",
|
||||
"Database - Tooltip": "Nome original do banco de dados",
|
||||
"Database type": "Tipo de banco de dados",
|
||||
"Database type - Tooltip": "Tipo de banco de dados, suportando todos os bancos de dados suportados pelo XORM, como MySQL, PostgreSQL, SQL Server, Oracle, SQLite, etc.",
|
||||
"Edit Syncer": "Editar Syncer",
|
||||
"Error text": "Texto de erro",
|
||||
"Error text - Tooltip": "Texto de erro",
|
||||
"Is hashed": "Está criptografado",
|
||||
"New Syncer": "Novo Syncer",
|
||||
"Sync interval": "Intervalo de sincronização",
|
||||
"Sync interval - Tooltip": "Unidade em segundos",
|
||||
"Table": "Tabela",
|
||||
"Table - Tooltip": "Nome da tabela no banco de dados",
|
||||
"Table columns": "Colunas da tabela",
|
||||
"Table columns - Tooltip": "Colunas na tabela envolvidas na sincronização de dados. Colunas que não estão envolvidas na sincronização não precisam ser adicionadas",
|
||||
"Table primary key": "Chave primária da tabela",
|
||||
"Table primary key - Tooltip": "Chave primária da tabela, como id"
|
||||
},
|
||||
"system": {
|
||||
"API Latency": "Latência da API",
|
||||
"API Throughput": "Throughput da API",
|
||||
"About Casdoor": "Sobre o Casdoor",
|
||||
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Uma plataforma de Gerenciamento de Identidade e Acesso (IAM) / Single-Sign-On (SSO) com interface web que suporta OAuth 2.0, OIDC, SAML e CAS",
|
||||
"CPU Usage": "Uso da CPU",
|
||||
"Community": "Comunidade",
|
||||
"Count": "Contagem",
|
||||
"Failed to get CPU usage": "Falha ao obter o uso da CPU",
|
||||
"Failed to get memory usage": "Falha ao obter o uso da memória",
|
||||
"Latency": "Latência",
|
||||
"Memory Usage": "Uso da Memória",
|
||||
"Official website": "Website oficial",
|
||||
"Throughput": "Throughput",
|
||||
"Total Throughput": "Total de Throughput",
|
||||
"Unknown version": "Versão desconhecida",
|
||||
"Version": "Versão"
|
||||
},
|
||||
"theme": {
|
||||
"Blossom": "Blossom",
|
||||
"Border radius": "Raio da borda",
|
||||
"Compact": "Compacto",
|
||||
"Customize theme": "Personalizar tema",
|
||||
"Dark": "Escuro",
|
||||
"Default": "Padrão",
|
||||
"Document": "Documento",
|
||||
"Is compact": "É compacto",
|
||||
"Primary color": "Cor primária",
|
||||
"Theme": "Tema",
|
||||
"Theme - Tooltip": "Tema de estilo do aplicativo"
|
||||
},
|
||||
"token": {
|
||||
"Access token": "Token de acesso",
|
||||
"Authorization code": "Código de autorização",
|
||||
"Edit Token": "Editar Token",
|
||||
"Expires in": "Expira em",
|
||||
"New Token": "Novo Token",
|
||||
"Token type": "Tipo de Token"
|
||||
},
|
||||
"user":
|
||||
{
|
||||
"3rd-party logins": "Logins de terceiros",
|
||||
"3rd-party logins - Tooltip": "Logins sociais vinculados pelo usuário",
|
||||
"Address": "Endereço",
|
||||
"Address - Tooltip": "Endereço residencial",
|
||||
"Affiliation": "Afiliação",
|
||||
"Affiliation - Tooltip": "Empregador, como nome da empresa ou organização",
|
||||
"Bio": "Biografia",
|
||||
"Bio - Tooltip": "Autoapresentação do usuário",
|
||||
"Birthday": "Aniversário",
|
||||
"Birthday - Tooltip": "Aniversário - Tooltip",
|
||||
"Captcha Verify Failed": "Falha na verificação de captcha",
|
||||
"Captcha Verify Success": "Verificação de captcha bem-sucedida",
|
||||
"Country code": "Código do país",
|
||||
"Country/Region": "País/Região",
|
||||
"Country/Region - Tooltip": "País ou região",
|
||||
"Edit User": "Editar Usuário",
|
||||
"Education": "Educação",
|
||||
"Education - Tooltip": "Educação - Tooltip",
|
||||
"Email cannot be empty": "O e-mail não pode ficar em branco",
|
||||
"Email/phone reset successfully": "Redefinição de e-mail/telefone com sucesso",
|
||||
"Empty input!": "Entrada vazia!",
|
||||
"Gender": "Gênero",
|
||||
"Gender - Tooltip": "Gênero - Tooltip",
|
||||
"Homepage": "Página inicial",
|
||||
"Homepage - Tooltip": "URL da página inicial do usuário",
|
||||
"ID card": "Cartão de identidade",
|
||||
"ID card - Tooltip": "Cartão de identidade - Tooltip",
|
||||
"ID card type": "Tipo de cartão de identidade",
|
||||
"ID card type - Tooltip": "Tipo de cartão de identidade - Tooltip",
|
||||
"Input your email": "Digite seu e-mail",
|
||||
"Input your phone number": "Digite seu número de telefone",
|
||||
"Is admin": "É administrador",
|
||||
"Is admin - Tooltip": "É um administrador da organização à qual o usuário pertence",
|
||||
"Is deleted": "Foi excluído",
|
||||
"Is deleted - Tooltip": "Usuários excluídos somente mantêm registros no banco de dados e não podem realizar nenhuma operação",
|
||||
"Is forbidden": "Está proibido",
|
||||
"Is forbidden - Tooltip": "Usuários proibidos não podem fazer login novamente",
|
||||
"Is global admin": "É administrador global",
|
||||
"Is global admin - Tooltip": "É um administrador do Casdoor",
|
||||
"Is online": "Está online",
|
||||
"Karma": "Karma",
|
||||
"Karma - Tooltip": "Karma - Tooltip",
|
||||
"Keys": "Chaves",
|
||||
"Language": "Idioma",
|
||||
"Language - Tooltip": "Idioma - Tooltip",
|
||||
"Link": "Link",
|
||||
"Location": "Localização",
|
||||
"Location - Tooltip": "Cidade de residência",
|
||||
"Managed accounts": "Contas gerenciadas",
|
||||
"Modify password...": "Modificar senha...",
|
||||
"Multi-factor authentication": "Autenticação de vários fatores",
|
||||
"New Email": "Novo E-mail",
|
||||
"New Password": "Nova Senha",
|
||||
"New User": "Novo Usuário",
|
||||
"New phone": "Novo telefone",
|
||||
"Old Password": "Senha Antiga",
|
||||
"Password set successfully": "Senha definida com sucesso",
|
||||
"Phone cannot be empty": "O telefone não pode ficar vazio",
|
||||
"Please select avatar from resources": "Selecione um avatar dos recursos",
|
||||
"Properties": "Propriedades",
|
||||
"Properties - Tooltip": "Propriedades do usuário",
|
||||
"Ranking": "Classificação",
|
||||
"Ranking - Tooltip": "Classificação - Tooltip",
|
||||
"Re-enter New": "Digite Novamente",
|
||||
"Reset Email...": "Redefinir E-mail...",
|
||||
"Reset Phone...": "Redefinir Telefone...",
|
||||
"Score": "Pontuação",
|
||||
"Score - Tooltip": "Pontuação - Tooltip",
|
||||
"Select a photo...": "Selecionar uma foto...",
|
||||
"Set Password": "Definir Senha",
|
||||
"Set new profile picture": "Definir nova foto de perfil",
|
||||
"Set password...": "Definir senha...",
|
||||
"Tag": "Tag",
|
||||
"Tag - Tooltip": "Tag do usuário",
|
||||
"Title": "Título",
|
||||
"Title - Tooltip": "Cargo na afiliação",
|
||||
"Two passwords you typed do not match.": "As duas senhas digitadas não coincidem.",
|
||||
"Unlink": "Desvincular",
|
||||
"Upload (.xlsx)": "Enviar (.xlsx)",
|
||||
"Upload a photo": "Enviar uma foto",
|
||||
"Values": "Valores",
|
||||
"Verification code sent": "Código de verificação enviado",
|
||||
"WebAuthn credentials": "Credenciais WebAuthn",
|
||||
"input password": "Digite a senha"
|
||||
},
|
||||
"webhook": {
|
||||
"Content type": "Tipo de conteúdo",
|
||||
"Content type - Tooltip": "Tipo de conteúdo",
|
||||
"Edit Webhook": "Editar Webhook",
|
||||
"Events": "Eventos",
|
||||
"Events - Tooltip": "Eventos",
|
||||
"Headers": "Cabeçalhos",
|
||||
"Headers - Tooltip": "Cabeçalhos HTTP (pares chave-valor)",
|
||||
"Is user extended": "É usuário estendido",
|
||||
"Is user extended - Tooltip": "Se incluir os campos estendidos do usuário no JSON",
|
||||
"Method - Tooltip": "Método HTTP",
|
||||
"New Webhook": "Novo Webhook",
|
||||
"Value": "Valor"
|
||||
}
|
||||
}
|
@ -253,6 +253,8 @@
|
||||
"Phone": "Телефон",
|
||||
"Phone - Tooltip": "Номер телефона",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "Планы",
|
||||
"Pricings": "Тарифы",
|
||||
"Preview": "Предварительный просмотр",
|
||||
"Preview - Tooltip": "Предварительный просмотр настроенных эффектов",
|
||||
"Products": "Продукты",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "К сожалению, у вас нет разрешения на доступ к этой странице или ваш статус входа недействителен.",
|
||||
"State": "Государство",
|
||||
"State - Tooltip": "Государство",
|
||||
"Subscriptions": "Подписки",
|
||||
"Successfully added": "Успешно добавлено",
|
||||
"Successfully deleted": "Успешно удалено",
|
||||
"Successfully saved": "Успешно сохранено",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "Метод HTTP",
|
||||
"New Webhook": "Новый вебхук",
|
||||
"Value": "Значение"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "Роль, включенная в текущий план",
|
||||
"PricePerMonth": "Цена за месяц",
|
||||
"PricePerYear": "Цена за год",
|
||||
"PerMonth": "в месяц"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "Тарифные планы",
|
||||
"Sub plans - Tooltip": "Планы, включенные в прайслист",
|
||||
"Has trial": "Есть пробный период",
|
||||
"Has trial - Tooltip": "Наличие пробного периода после выбора плана",
|
||||
"Trial duration": "Продолжительность пробного периода",
|
||||
"Trial duration - Tooltip": "Продолжительность пробного периода",
|
||||
"Getting started": "Выьрать план",
|
||||
"Copy pricing page URL": "Скопировать URL прайс-листа",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL страницы прайс-листа успешно скопирован в буфер обмена, пожалуйста, вставьте его в режиме инкогнито или другом браузере",
|
||||
"days trial available!": "дней пробного периода доступно!",
|
||||
"Free": "Бесплатно"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "Продолжительность",
|
||||
"Duration - Tooltip": "Продолжительность подписки",
|
||||
"Start Date": "Дата начала",
|
||||
"Start Date - Tooltip": "Дата начала",
|
||||
"End Date": "Дата окончания",
|
||||
"End Date - Tooltip": "Дата окончания",
|
||||
"Sub users": "Пользователь подписки",
|
||||
"Sub users - Tooltip": "Пользователь которому офомлена подписка",
|
||||
"Sub plan": "План подписки",
|
||||
"Sub plan - Tooltip": "План подписки"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "Điện thoại",
|
||||
"Phone - Tooltip": "Số điện thoại",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plans": "Kế hoạch",
|
||||
"Pricings": "Bảng giá",
|
||||
"Preview": "Xem trước",
|
||||
"Preview - Tooltip": "Xem trước các hiệu ứng đã cấu hình",
|
||||
"Products": "Sản phẩm",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "Xin lỗi, bạn không có quyền truy cập trang này hoặc trạng thái đăng nhập không hợp lệ.",
|
||||
"State": "Nhà nước",
|
||||
"State - Tooltip": "Trạng thái",
|
||||
"Subscriptions": "Đăng ký",
|
||||
"Successfully added": "Đã thêm thành công",
|
||||
"Successfully deleted": "Đã xóa thành công",
|
||||
"Successfully saved": "Thành công đã được lưu lại",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "Phương thức HTTP",
|
||||
"New Webhook": "Webhook mới",
|
||||
"Value": "Giá trị"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "Vai trò bao gồm trong kế hoạch hiện tại",
|
||||
"PricePerMonth": "Giá mỗi tháng",
|
||||
"PricePerYear": "Giá mỗi năm",
|
||||
"PerMonth": "mỗi tháng"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "Kế hoạch phụ",
|
||||
"Sub plans - Tooltip": "Các kế hoạch bao gồm trong bảng giá hiện tại",
|
||||
"Has trial": "Có thời gian thử nghiệm",
|
||||
"Has trial - Tooltip": "Khả dụng thời gian thử nghiệm sau khi chọn kế hoạch",
|
||||
"Trial duration": "Thời gian thử nghiệm",
|
||||
"Trial duration - Tooltip": "Thời gian thử nghiệm",
|
||||
"Getting started": "Bắt đầu",
|
||||
"Copy pricing page URL": "Sao chép URL trang bảng giá",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL trang bảng giá đã được sao chép vào clipboard thành công, vui lòng dán vào cửa sổ ẩn danh hoặc trình duyệt khác",
|
||||
"days trial available!": "ngày dùng thử có sẵn!",
|
||||
"Free": "Miễn phí"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "Thời lượng",
|
||||
"Duration - Tooltip": "Thời lượng đăng ký",
|
||||
"Start Date": "Ngày bắt đầu",
|
||||
"Start Date - Tooltip": "Ngày bắt đầu",
|
||||
"End Date": "Ngày kết thúc",
|
||||
"End Date - Tooltip": "Ngày kết thúc",
|
||||
"Sub users": "Người dùng đăng ký",
|
||||
"Sub users - Tooltip": "Người dùng đăng ký",
|
||||
"Sub plan": "Kế hoạch đăng ký",
|
||||
"Sub plan - Tooltip": "Kế hoạch đăng ký"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,8 @@
|
||||
"Phone": "手机号",
|
||||
"Phone - Tooltip": "手机号",
|
||||
"Phone or email": "手机或邮箱",
|
||||
"Plans": "计划",
|
||||
"Pricings": "定价",
|
||||
"Preview": "预览",
|
||||
"Preview - Tooltip": "可预览所配置的效果",
|
||||
"Products": "商品",
|
||||
@ -281,6 +283,7 @@
|
||||
"Sorry, you do not have permission to access this page or logged in status invalid.": "抱歉,您无权访问该页面或登录状态失效",
|
||||
"State": "状态",
|
||||
"State - Tooltip": "状态",
|
||||
"Subscriptions": "订阅",
|
||||
"Successfully added": "添加成功",
|
||||
"Successfully deleted": "删除成功",
|
||||
"Successfully saved": "保存成功",
|
||||
@ -884,5 +887,36 @@
|
||||
"Method - Tooltip": "HTTP方法",
|
||||
"New Webhook": "添加Webhook",
|
||||
"Value": "值"
|
||||
},
|
||||
"plan": {
|
||||
"Sub roles - Tooltip": "当前计划中包含的角色",
|
||||
"PricePerMonth": "每月价格",
|
||||
"PricePerYear": "每年价格",
|
||||
"PerMonth": "每月"
|
||||
},
|
||||
"pricing": {
|
||||
"Sub plans": "附加计划",
|
||||
"Sub plans - Tooltip": "包含在当前定价中的计划",
|
||||
"Has trial": "有试用期",
|
||||
"Has trial - Tooltip": "选择计划后是否有试用期",
|
||||
"Trial duration": "试用期时长",
|
||||
"Trial duration - Tooltip": "试用期时长",
|
||||
"Getting started": "开始使用",
|
||||
"Copy pricing page URL": "复制定价页面链接",
|
||||
"pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "定价页面链接已成功复制到剪贴板,请粘贴到隐身窗口或其他浏览器中",
|
||||
"days trial available!": "天试用期可用!",
|
||||
"Free": "免费"
|
||||
},
|
||||
"subscription": {
|
||||
"Duration": "订阅时长",
|
||||
"Duration - Tooltip": "订阅时长",
|
||||
"Start Date": "开始日期",
|
||||
"Start Date - Tooltip": "开始日期",
|
||||
"End Date": "结束日期",
|
||||
"End Date - Tooltip": "结束日期",
|
||||
"Sub users": "订阅用户",
|
||||
"Sub users - Tooltip": "订阅用户",
|
||||
"Sub plan": "订阅计划",
|
||||
"Sub plan - Tooltip": "订阅计划"
|
||||
}
|
||||
}
|
||||
|
167
web/src/pricing/PricingPage.js
Normal file
167
web/src/pricing/PricingPage.js
Normal file
@ -0,0 +1,167 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Card, Col, Row} from "antd";
|
||||
import * as PricingBackend from "../backend/PricingBackend";
|
||||
import * as PlanBackend from "../backend/PlanBackend";
|
||||
import CustomGithubCorner from "../common/CustomGithubCorner";
|
||||
import * as Setting from "../Setting";
|
||||
import SingleCard from "./SingleCard";
|
||||
import i18next from "i18next";
|
||||
|
||||
class PricingPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
applications: null,
|
||||
pricingName: (props.pricingName ?? props.match?.params?.pricingName) ?? null,
|
||||
pricing: props.pricing,
|
||||
plans: null,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
applications: [],
|
||||
});
|
||||
|
||||
if (this.state.pricing) {
|
||||
this.loadPlans();
|
||||
} else {
|
||||
this.loadPricing(this.state.pricingName);
|
||||
}
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.pricing &&
|
||||
this.state.pricing.plans?.length !== this.state.plans?.length && !this.state.loading) {
|
||||
this.setState({loading: true});
|
||||
this.loadPlans();
|
||||
}
|
||||
}
|
||||
|
||||
loadPlans() {
|
||||
const plans = this.state.pricing.plans.map((plan) =>
|
||||
PlanBackend.getPlanById(plan, true));
|
||||
|
||||
Promise.all(plans)
|
||||
.then(results => {
|
||||
this.setState({
|
||||
plans: results,
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Failed to get plans: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
loadPricing(pricingName) {
|
||||
if (pricingName === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
PricingBackend.getPricing("built-in", pricingName)
|
||||
.then((result) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
pricing: result,
|
||||
});
|
||||
this.onUpdatePricing(result);
|
||||
});
|
||||
}
|
||||
|
||||
onUpdatePricing(pricing) {
|
||||
this.props.onUpdatePricing(pricing);
|
||||
}
|
||||
|
||||
renderCards() {
|
||||
|
||||
const getUrlByPlan = (plan) => {
|
||||
const pricing = this.state.pricing;
|
||||
const signUpUrl = `/signup/${pricing.application}?plan=${plan}&pricing=${pricing.name}`;
|
||||
return `${window.location.origin}${signUpUrl}`;
|
||||
};
|
||||
|
||||
if (Setting.isMobile()) {
|
||||
return (
|
||||
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
|
||||
{
|
||||
this.state.plans.map(item => {
|
||||
return (
|
||||
<SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{marginRight: "15px", marginLeft: "15px"}}>
|
||||
<Row style={{justifyContent: "center"}} gutter={24}>
|
||||
{
|
||||
this.state.plans.map(item => {
|
||||
return (
|
||||
<SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading || this.state.plans === null || this.state.plans === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pricing = this.state.pricing;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CustomGithubCorner />
|
||||
<div className="login-content">
|
||||
<div className="login-panel">
|
||||
<div className="login-form">
|
||||
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
|
||||
<span style={{fontSize: "20px"}}>{pricing.description}</span>
|
||||
<Row style={{width: "100%", marginTop: "40px"}}>
|
||||
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||
{
|
||||
this.renderCards()
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{justifyContent: "center"}}>
|
||||
{pricing && pricing.trialDuration > 0
|
||||
? <i>{i18next.t("pricing:Free")} {pricing.trialDuration}-{i18next.t("pricing:days trial available!")}</i>
|
||||
: null}
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PricingPage;
|
85
web/src/pricing/SingleCard.js
Normal file
85
web/src/pricing/SingleCard.js
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import {Button, Card, Col} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import {withRouter} from "react-router-dom";
|
||||
|
||||
const {Meta} = Card;
|
||||
|
||||
class SingleCard extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
};
|
||||
}
|
||||
|
||||
renderCard(plan, isSingle, link) {
|
||||
|
||||
return (
|
||||
<Col style={{minWidth: "320px", paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px", paddingTop: "0px"}} span={6}>
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => Setting.isMobile() ? window.location.href = link : null}
|
||||
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
|
||||
>
|
||||
<div style={{textAlign: "right"}}>
|
||||
<h2
|
||||
style={{marginTop: "0px"}}>{plan.displayName}</h2>
|
||||
</div>
|
||||
|
||||
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
||||
<span style={{fontWeight: 700, fontSize: "48px"}}>$ {plan.pricePerMonth}</span>
|
||||
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:PerMonth")}</span>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div style={{textAlign: "left", fontSize: "18px"}}>
|
||||
<Meta description={plan.description} />
|
||||
</div>
|
||||
<br />
|
||||
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
||||
{(plan.options ?? []).map((option) => {
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
return <li>
|
||||
<svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path>
|
||||
</svg>
|
||||
<span style={{fontSize: "16px"}}>{option}</span>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
<div style={{minHeight: "60px"}}>
|
||||
|
||||
</div>
|
||||
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
|
||||
{
|
||||
i18next.t("pricing:Getting started")
|
||||
}
|
||||
</Button>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.renderCard(this.props.plan, this.props.isSingle, this.props.link);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(SingleCard);
|
160
web/src/table/MfaTable.js
Normal file
160
web/src/table/MfaTable.js
Normal file
@ -0,0 +1,160 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Row, Select, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const MfaItems = [
|
||||
{name: "Phone"},
|
||||
{name: "Email"},
|
||||
];
|
||||
|
||||
class MfaTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
};
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {name: Setting.getNewRowNameForTable(table, "Please select a MFA method"), rule: "Optional"};
|
||||
if (table === undefined) {
|
||||
table = [];
|
||||
}
|
||||
table = Setting.addRow(table, row);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
deleteRow(table, i) {
|
||||
table = Setting.deleteRow(table, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
upRow(table, i) {
|
||||
table = Setting.swapRow(table, i - 1, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
downRow(table, i) {
|
||||
table = Setting.swapRow(table, i, i + 1);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={text}
|
||||
onChange={value => {
|
||||
this.updateField(table, index, "name", value);
|
||||
}} >
|
||||
{
|
||||
Setting.getDeduplicatedArray(MfaItems, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("application:Rule"),
|
||||
dataIndex: "rule",
|
||||
key: "rule",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={text}
|
||||
defaultValue="Optional"
|
||||
options={[
|
||||
{value: "Optional", label: i18next.t("organization:Optional")},
|
||||
{value: "Required", label: i18next.t("organization:Required")}].map((item) =>
|
||||
Setting.getOption(item.label, item.value))
|
||||
}
|
||||
onChange={value => {
|
||||
this.updateField(table, index, "rule", value);
|
||||
}} >
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
|
||||
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table scroll={{x: "max-content"}} rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false}
|
||||
title={() => (
|
||||
<div>
|
||||
{this.props.title}
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={24}>
|
||||
{
|
||||
this.renderTable(this.props.table)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MfaTable;
|
@ -60,6 +60,10 @@ class UrlTable extends React.Component {
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
if (table === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("application:Redirect URL"),
|
||||
|
Reference in New Issue
Block a user