diff --git a/authz/authz.go b/authz/authz.go index 95995670..07605c3c 100644 --- a/authz/authz.go +++ b/authz/authz.go @@ -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) diff --git a/controllers/account.go b/controllers/account.go index 152fc93f..567f2072 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -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 diff --git a/controllers/plan.go b/controllers/plan.go new file mode 100644 index 00000000..0536966b --- /dev/null +++ b/controllers/plan.go @@ -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() +} diff --git a/controllers/pricing.go b/controllers/pricing.go new file mode 100644 index 00000000..01ed0c4c --- /dev/null +++ b/controllers/pricing.go @@ -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() +} diff --git a/controllers/subscription.go b/controllers/subscription.go new file mode 100644 index 00000000..1216c0af --- /dev/null +++ b/controllers/subscription.go @@ -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() +} diff --git a/form/auth.go b/form/auth.go index b9a0ce00..30aba864 100644 --- a/form/auth.go +++ b/form/auth.go @@ -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"` } diff --git a/object/adapter.go b/object/adapter.go index 0b3545ba..53ec8303 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -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 { diff --git a/object/plan.go b/object/plan.go new file mode 100644 index 00000000..1c86d1a6 --- /dev/null +++ b/object/plan.go @@ -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 +} diff --git a/object/pricing.go b/object/pricing.go new file mode 100644 index 00000000..24b20dec --- /dev/null +++ b/object/pricing.go @@ -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 +} diff --git a/object/subscription.go b/object/subscription.go new file mode 100644 index 00000000..5724fb29 --- /dev/null +++ b/object/subscription.go @@ -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) +} diff --git a/routers/router.go b/routers/router.go index 5a7bed01..748d992d 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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") diff --git a/swagger/swagger.json b/swagger/swagger.json index 38ae93b3..b99e8fa7 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -275,6 +275,62 @@ } } }, + "/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": [ @@ -431,6 +487,34 @@ } } }, + "/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": [ @@ -854,6 +938,23 @@ } } }, + "/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": [ @@ -966,6 +1067,62 @@ } } }, + "/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": [ @@ -1087,6 +1244,34 @@ } } }, + "/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": [ @@ -1534,6 +1719,26 @@ } } }, + "/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": [ @@ -1584,6 +1789,32 @@ } } }, + "/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": [ @@ -1911,6 +2142,122 @@ } } }, + "/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": [ @@ -1966,6 +2313,23 @@ } } }, + "/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": [ @@ -2254,6 +2618,61 @@ } } }, + "/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": [ @@ -3022,6 +3441,57 @@ } } }, + "/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": [ @@ -3133,6 +3603,23 @@ } } }, + "/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": [ @@ -3469,6 +3956,76 @@ } } }, + "/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": [ @@ -3611,6 +4168,41 @@ } } }, + "/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": [ @@ -3909,11 +4501,11 @@ } }, "definitions": { - "1183.0xc000455050.false": { + "1183.0x1400042eb70.false": { "title": "false", "type": "object" }, - "1217.0xc000455080.false": { + "1217.0x1400042eba0.false": { "title": "false", "type": "object" }, @@ -3925,6 +4517,10 @@ "title": "Response", "type": "object" }, + "The": { + "title": "The", + "type": "object" + }, "controllers.AuthForm": { "title": "AuthForm", "type": "object" @@ -3958,10 +4554,10 @@ "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" @@ -4269,6 +4865,22 @@ } } }, + "object.GaugeVecInfo": { + "title": "GaugeVecInfo", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "throughput": { + "type": "number", + "format": "double" + } + } + }, "object.Header": { "title": "Header", "type": "object", @@ -4281,6 +4893,25 @@ } } }, + "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", @@ -4369,11 +5000,44 @@ "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", @@ -4715,6 +5379,96 @@ } } }, + "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", @@ -4778,6 +5532,28 @@ } } }, + "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", @@ -5035,6 +5811,60 @@ } } }, + "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", @@ -5478,6 +6308,12 @@ "microsoftonline": { "type": "string" }, + "multiFactorAuths": { + "type": "array", + "items": { + "$ref": "#/definitions/object.MfaProps" + } + }, "name": { "type": "string" }, @@ -5728,6 +6564,14 @@ } } }, + "object.pricing": { + "title": "pricing", + "type": "object" + }, + "object.subscription": { + "title": "subscription", + "type": "object" + }, "protocol.CredentialAssertion": { "title": "CredentialAssertion", "type": "object" @@ -5790,4 +6634,4 @@ "type": "object" } } -} \ No newline at end of file +} diff --git a/swagger/swagger.yml b/swagger/swagger.yml index bdf90465..c4a2998e 100644 --- a/swagger/swagger.yml +++ b/swagger/swagger.yml @@ -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 diff --git a/web/src/App.js b/web/src/App.js index e1c9a929..e5aa5c98 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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({i18next.t("general:Records")}, "/records" )); + + res.push(Setting.getItem({i18next.t("general:Plans")}, + "/plans" + )); + + res.push(Setting.getItem({i18next.t("general:Pricings")}, + "/pricings" + )); + + res.push(Setting.getItem({i18next.t("general:Subscriptions")}, + "/subscriptions" + )); + } if (Setting.isLocalAdminUser(this.state.account)) { @@ -468,6 +495,7 @@ class App extends Component { )); if (Conf.EnableExtraPages) { + res.push(Setting.getItem({i18next.t("general:Products")}, "/products" )); @@ -556,6 +584,12 @@ class App extends Component { this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> @@ -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() { diff --git a/web/src/EntryPage.js b/web/src/EntryPage.js index 0b81fa4b..1b67ee2f 100644 --- a/web/src/EntryPage.js +++ b/web/src/EntryPage.js @@ -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 ( -
- +
+ this.renderHomeIfLoggedIn()} /> this.renderHomeIfLoggedIn()} /> @@ -85,6 +102,7 @@ class EntryPage extends React.Component { this.renderHomeIfLoggedIn()} /> this.renderHomeIfLoggedIn()} /> {return ();}} /> + this.renderHomeIfLoggedIn()} />
); diff --git a/web/src/PlanEditPage.js b/web/src/PlanEditPage.js new file mode 100644 index 00000000..86541b61 --- /dev/null +++ b/web/src/PlanEditPage.js @@ -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 ( + + {this.state.mode === "add" ? i18next.t("plan:New Plan") : i18next.t("plan:Edit Plan")}     + + + {this.state.mode === "add" ? : null} +
+ } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> + + + {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : + + + { + this.updatePlanField("name", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : + + + { + this.updatePlanField("displayName", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("plan:Sub roles - Tooltip"))} : + + + { + this.updatePlanField("description", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("plan:PricePerMonth"), i18next.t("plan:PricePerMonth - Tooltip"))} : + + + { + this.updatePlanField("pricePerMonth", value); + }} /> + + + + + {Setting.getLabel(i18next.t("plan:PricePerYear"), i18next.t("plan:PricePerYear - Tooltip"))} : + + + { + this.updatePlanField("pricePerYear", value); + }} /> + + + + + {Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : + + + { + this.updatePlanField("isEnabled", checked); + }} /> + + + + ); + } + + 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 ( +
+ { + this.state.plan !== null ? this.renderPlan() : null + } +
+ + + {this.state.mode === "add" ? : null} +
+
+ ); + } +} + +export default PlanEditPage; diff --git a/web/src/PlanListPage.js b/web/src/PlanListPage.js new file mode 100644 index 00000000..049c81ac --- /dev/null +++ b/web/src/PlanListPage.js @@ -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 ( + + {text} + + ); + }, + }, + { + title: i18next.t("general:Organization"), + dataIndex: "owner", + key: "owner", + width: "120px", + sorter: true, + ...this.getColumnSearchProps("owner"), + render: (text, record, index) => { + return ( + + {text} + + ); + }, + }, + { + 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 ( + + ); + }, + }, + { + title: i18next.t("general:Action"), + dataIndex: "", + key: "op", + width: "200px", + fixed: (Setting.isMobile()) ? "false" : "right", + render: (text, record, index) => { + return ( +
+ + this.deletePlan(index)} + > + +
+ ); + }, + }, + ]; + + 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 ( +
+ ( +
+ {i18next.t("general:Plans")}     + +
+ )} + loading={this.state.loading} + onChange={this.handleTableChange} + /> + + ); + } + + 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; diff --git a/web/src/PricingEditPage.js b/web/src/PricingEditPage.js new file mode 100644 index 00000000..a1d2e558 --- /dev/null +++ b/web/src/PricingEditPage.js @@ -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 ( + + {this.state.mode === "add" ? i18next.t("pricing:New Pricing") : i18next.t("pricing:Edit Pricing")}     + + + {this.state.mode === "add" ? : null} + + } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> + + + {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : + + + { + this.updatePricingField("name", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : + + + { + this.updatePricingField("displayName", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} : + + + { + this.updatePricingField("description", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} : + + + { + this.updatePricingField("plans", value); + })} + options={this.state.plans.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`))} + /> + + + + + {Setting.getLabel(i18next.t("pricing:Has trial"), i18next.t("pricing:Has trial - Tooltip"))} : + + + { + this.updatePricingField("hasTrial", checked); + }} /> + + + + + {Setting.getLabel(i18next.t("pricing:Trial duration"), i18next.t("pricing:Trial duration - Tooltip"))} : + + + { + this.updatePricingField("trialDuration", value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : + + + { + this.updatePricingField("isEnabled", checked); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : + + { + this.renderPreview() + } + + + ); + } + + 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 ( +
+ { + this.state.pricing !== null ? this.renderPricing() : null + } +
+ + + {this.state.mode === "add" ? : null} +
+
+ ); + } + + renderPreview() { + const pricingUrl = `/select-plan/${this.state.pricing.name}`; + return ( + + + + + + + + + ); + } +} + +export default PricingEditPage; diff --git a/web/src/PricingListPage.js b/web/src/PricingListPage.js new file mode 100644 index 00000000..ddd3a460 --- /dev/null +++ b/web/src/PricingListPage.js @@ -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 ( + + {text} + + ); + }, + }, + { + title: i18next.t("general:Organization"), + dataIndex: "owner", + key: "owner", + width: "120px", + sorter: true, + ...this.getColumnSearchProps("owner"), + render: (text, record, index) => { + return ( + + {text} + + ); + }, + }, + { + 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 ( + + ); + }, + }, + { + title: i18next.t("general:Action"), + dataIndex: "", + key: "op", + width: "230px", + fixed: (Setting.isMobile()) ? "false" : "right", + render: (text, record, index) => { + return ( +
+ + this.deletePricing(index)} + > + +
+ ); + }, + }, + ]; + + 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 ( +
+
( +
+ {i18next.t("general:Pricings")}     + +
+ )} + loading={this.state.loading} + onChange={this.handleTableChange} + /> + + ); + } + + 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; diff --git a/web/src/SubscriptionEditPage.js b/web/src/SubscriptionEditPage.js new file mode 100644 index 00000000..54801229 --- /dev/null +++ b/web/src/SubscriptionEditPage.js @@ -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 ( + + {this.state.mode === "add" ? i18next.t("subscription:New Subscription") : i18next.t("subscription:Edit Subscription")}     + + + {this.state.mode === "add" ? : null} + + } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> + + + {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : + + + { + this.updateSubscriptionField("name", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : + + + { + this.updateSubscriptionField("displayName", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("subscription:Duration"), i18next.t("subscription:Duration - Tooltip"))} + + + { + this.updateSubscriptionField("duration", value); + }} /> + + + + + {Setting.getLabel(i18next.t("subscription:Start Date"), i18next.t("subscription:Start Date - Tooltip"))} + + + { + this.updateSubscriptionField("startDate", value); + }} /> + + + + + {Setting.getLabel(i18next.t("subscription:End Date"), i18next.t("subscription:End Date - Tooltip"))} + + + { + this.updateSubscriptionField("endDate", value); + }} /> + + + + + {Setting.getLabel(i18next.t("subscription:Sub users"), i18next.t("subscription:Sub users - Tooltip"))} : + + + {this.updateSubscriptionField("plan", value);})} + options={this.state.planes.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`)) + } /> + + + + + {Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} : + + + { + this.updateSubscriptionField("description", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : + + + { + this.updateSubscriptionField("isEnabled", checked); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Submitter"), i18next.t("general:Submitter - Tooltip"))} : + + + { + this.updateSubscriptionField("submitter", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Approver"), i18next.t("general:Approver - Tooltip"))} : + + + { + this.updateSubscriptionField("approver", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Approve time"), i18next.t("general:Approve time - Tooltip"))} : + + + { + this.updatePermissionField("approveTime", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} : + + +
( +
+ {i18next.t("general:Subscriptions")}     + +
+ )} + loading={this.state.loading} + onChange={this.handleTableChange} + /> + + ); + } + + 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; diff --git a/web/src/WebhookEditPage.js b/web/src/WebhookEditPage.js index 36b647d7..18b52493 100644 --- a/web/src/WebhookEditPage.js +++ b/web/src/WebhookEditPage.js @@ -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 ( ); diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js index d5853c83..f9776525 100644 --- a/web/src/auth/LoginPage.js +++ b/web/src/auth/LoginPage.js @@ -438,6 +438,7 @@ class LoginPage extends React.Component { { if (res.status === "ok") { diff --git a/web/src/backend/PlanBackend.js b/web/src/backend/PlanBackend.js new file mode 100644 index 00000000..e56ebb1c --- /dev/null +++ b/web/src/backend/PlanBackend.js @@ -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()); +} diff --git a/web/src/backend/PricingBackend.js b/web/src/backend/PricingBackend.js new file mode 100644 index 00000000..dbf3cd3d --- /dev/null +++ b/web/src/backend/PricingBackend.js @@ -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()); +} diff --git a/web/src/backend/SubscriptionBackend.js b/web/src/backend/SubscriptionBackend.js new file mode 100644 index 00000000..72adea02 --- /dev/null +++ b/web/src/backend/SubscriptionBackend.js @@ -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()); +} diff --git a/web/src/locales/de/data.json b/web/src/locales/de/data.json index 79138b5b..c38943ee 100644 --- a/web/src/locales/de/data.json +++ b/web/src/locales/de/data.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" } } diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index 9187155c..9ce43052 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -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" } } diff --git a/web/src/locales/es/data.json b/web/src/locales/es/data.json index 9035aed8..b20863e2 100644 --- a/web/src/locales/es/data.json +++ b/web/src/locales/es/data.json @@ -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" } } diff --git a/web/src/locales/fr/data.json b/web/src/locales/fr/data.json index a60e7f4a..e6a2a03a 100644 --- a/web/src/locales/fr/data.json +++ b/web/src/locales/fr/data.json @@ -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" } } diff --git a/web/src/locales/id/data.json b/web/src/locales/id/data.json index a70a505d..a7b8594c 100644 --- a/web/src/locales/id/data.json +++ b/web/src/locales/id/data.json @@ -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" } } diff --git a/web/src/locales/ja/data.json b/web/src/locales/ja/data.json index 34376a55..69f723a1 100644 --- a/web/src/locales/ja/data.json +++ b/web/src/locales/ja/data.json @@ -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": "購読プラン" } } diff --git a/web/src/locales/ko/data.json b/web/src/locales/ko/data.json index 11b9f700..23c544f1 100644 --- a/web/src/locales/ko/data.json +++ b/web/src/locales/ko/data.json @@ -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": "구독 플랜" } } diff --git a/web/src/locales/ru/data.json b/web/src/locales/ru/data.json index 44786ab2..1892c38d 100644 --- a/web/src/locales/ru/data.json +++ b/web/src/locales/ru/data.json @@ -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": "План подписки" } } diff --git a/web/src/locales/vi/data.json b/web/src/locales/vi/data.json index db97854e..24e81e4b 100644 --- a/web/src/locales/vi/data.json +++ b/web/src/locales/vi/data.json @@ -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ý" } } diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index 02ca5d14..73db3aa7 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -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": "订阅计划" } } diff --git a/web/src/pricing/PricingPage.js b/web/src/pricing/PricingPage.js new file mode 100644 index 00000000..e9c07e76 --- /dev/null +++ b/web/src/pricing/PricingPage.js @@ -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 ( + + { + this.state.plans.map(item => { + return ( + + ); + }) + } + + ); + } else { + return ( +
+ + { + this.state.plans.map(item => { + return ( + + ); + }) + } + +
+ ); + } + } + + render() { + if (this.state.loading || this.state.plans === null || this.state.plans === undefined) { + return null; + } + + const pricing = this.state.pricing; + + return ( + + +
+
+
+

{pricing.displayName}

+ {pricing.description} + +
+ { + this.renderCards() + } + + + + {pricing && pricing.trialDuration > 0 + ? {i18next.t("pricing:Free")} {pricing.trialDuration}-{i18next.t("pricing:days trial available!")} + : null} + + + + + + ); + } +} + +export default PricingPage; diff --git a/web/src/pricing/SingleCard.js b/web/src/pricing/SingleCard.js new file mode 100644 index 00000000..99ec0068 --- /dev/null +++ b/web/src/pricing/SingleCard.js @@ -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 ( + + Setting.isMobile() ? window.location.href = link : null} + style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}} + > +
+

{plan.displayName}

+
+ +
+ $ {plan.pricePerMonth} + {i18next.t("plan:PerMonth")} +
+ +
+
+ +
+
+
    + {(plan.options ?? []).map((option) => { + // eslint-disable-next-line react/jsx-key + return
  • + + + + {option} +
  • ; + })} +
+
+ +
+ +
+ + ); + } + + render() { + return this.renderCard(this.props.plan, this.props.isSingle, this.props.link); + } +} + +export default withRouter(SingleCard);