mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-02 19:40:19 +08:00
feat: add subscription managment (#1858)
* feat: subscription managment * fix: remove console log * fix: webhooks * fix linter * fix: fix via gofumpt * fix: review changes * fix: Copyright 2023 * Update account.go --------- Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
@ -124,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)
|
||||
|
@ -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
|
||||
|
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()
|
||||
}
|
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()
|
||||
}
|
@ -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"`
|
||||
}
|
||||
|
@ -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 {
|
||||
|
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
|
||||
}
|
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)
|
||||
}
|
@ -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")
|
||||
|
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"
|
||||
));
|
||||
@ -556,6 +584,12 @@ class App extends Component {
|
||||
<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} />)} />
|
||||
@ -674,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() {
|
||||
|
@ -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>
|
||||
);
|
||||
|
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;
|
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;
|
@ -259,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>
|
||||
);
|
||||
|
@ -438,6 +438,7 @@ class LoginPage extends React.Component {
|
||||
<Form
|
||||
name="normal_login"
|
||||
initialValues={{
|
||||
|
||||
organization: application.organization,
|
||||
application: application.name,
|
||||
autoSignin: true,
|
||||
|
@ -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") {
|
||||
|
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());
|
||||
}
|
@ -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": "구독 플랜"
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
Reference in New Issue
Block a user