Compare commits

...

34 Commits

Author SHA1 Message Date
Cattī Crūdēlēs
0c5c308071 fix: sendCasAuthenticationResponseErr when pgtUrlObj if not valid url (#2287)
* fix: sendCasAuthenticationResponseErr when pgtUrlObj if not valid url

check pgtUrlObj.Scheme first will cause panic if url.Parse returns error.

* Update cas.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-09-01 22:26:57 +08:00
Yang Luo
0b859197da Fix CAS "/proxyValidate" API 2023-09-01 21:47:26 +08:00
Yang Luo
3078409343 Add CertPublicKey to Application 2023-09-01 21:16:51 +08:00
Tower He
bbf2db2e00 feat: support to use a different db schema for pg (#2281) 2023-09-01 18:02:13 +08:00
Yang Luo
0c7b911ce7 Fix enforcer edit page logic 2023-09-01 01:30:50 +08:00
Yang Luo
2cc55715ac Add app.conf existence check 2023-09-01 01:25:45 +08:00
Yang Luo
c829bf1769 Fix DummyPaymentProvider's return URL 2023-09-01 01:25:15 +08:00
Yang Luo
ec956c12ca Fix Email duplicated issue in update-user 2023-08-31 23:44:40 +08:00
Tower He
d3d4646c56 feat: fix can not create db when using pg with a dbname in DSN (#2280)
* fix: can not create db when using pg with a dbname in DSN

* Update ormer.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-08-31 18:05:38 +08:00
Yang Luo
669ac7c618 Don't encrypt user pass when user.PasswordType is non-empty when adding users 2023-08-31 17:49:36 +08:00
Yang Luo
6715efd781 Fix enforcer edit page 2023-08-31 17:32:36 +08:00
haiwu
953be4a7b6 feat: support subscription periods (yearly/monthly) (#2265)
* feat: support year/month subscription

* feat: add GetPrice() for plan

* feat: add GetDuration

* feat: gofumpt

* feat: add subscription mode for pricing

* feat: restrict auto create product operation

* fix: format code

* feat: add period for plan,remove period from pricing

* feat: format code

* feat: remove space

* feat: remove period in signup page
2023-08-30 17:13:45 +08:00
Yang Luo
943cc43427 Fix payment list and product edit actions 2023-08-28 21:01:23 +08:00
Yang Luo
1e5ce7a045 Fix crash in syncUsersNoError() 2023-08-28 01:51:06 +08:00
Baihhh
7a85b74573 fix: fix tour disabled state (#2264)
* fix: distinguish between pages that can tour or not

* Update OpenTour.js

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-08-27 23:18:14 +08:00
Yang Luo
7e349c1768 feat: fix crash bug in getSteps() 2023-08-27 21:58:58 +08:00
Baihhh
b19be2df88 fix: change the id to key in syncer (#2263) 2023-08-27 20:57:27 +08:00
Yang Luo
fc3866db1c Use XORM grammar in syncer 2023-08-27 18:15:23 +08:00
Yang Luo
bf2bb31e41 Add sslMode for syncer 2023-08-27 17:07:19 +08:00
Baihhh
ec8bd6f01d feat: add tour for list pages (#2243) 2023-08-27 16:40:31 +08:00
Yang Luo
98722fd681 Fix crash in app list page for normal user 2023-08-27 11:31:48 +08:00
Yang Luo
221c55aa93 Fix yarn build cmd 2023-08-27 11:17:18 +08:00
Yang Luo
988b26b3c2 Return error for RunSyncer() 2023-08-27 02:22:37 +08:00
Yang Luo
7e3c361ce7 Add all webhook events 2023-08-26 23:50:24 +08:00
Yang Luo
a637707e77 Fix null bug in IsAdminOrSelf() 2023-08-26 10:39:46 +08:00
Yaodong Yu
7970edeaa7 feat: password and invitation code verification rules (#2258) 2023-08-25 21:16:21 +08:00
haiwu
9da2f0775f fix: fix bug in Pricing (#2255) 2023-08-25 19:27:46 +08:00
Yang Luo
739a9bcd0d feat: add CasvisorUrl 2023-08-25 11:56:12 +08:00
Yang Luo
fb0949b9ed Fix docker cannot get version bug 2023-08-25 11:49:47 +08:00
Yang Luo
27ed901167 Restrict sysinfo page to global admin 2023-08-25 11:20:11 +08:00
Yang Luo
ceab662b88 Remove dup swagger page 2023-08-25 11:09:59 +08:00
haiwu
05b2f00057 feat: support Pricings flow (#2250)
* feat: fix price display

* feat: support subscription

* feat: fix select-plan-> signup -> buy-plan -> login flow

* feat: support paid-user to login and jump to the pricing page

* feat: support more subscription state

* feat: add payment providers for plan

* feat: format code

* feat: gofumpt

* feat: redirect to buy-plan-result page when user have pending subscription

* feat: response err when pricing don't exit

* Update PricingListPage.js

* Update ProductBuyPage.js

* Update LoginPage.js

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-08-24 23:20:50 +08:00
Yang Luo
8073dfa88c Remove tmpFiles folder usage 2023-08-24 22:03:36 +08:00
Yang Luo
1eeeb64a0c Add checkModel() for UserGroupEnforcer 2023-08-24 18:22:23 +08:00
84 changed files with 1791 additions and 713 deletions

View File

@@ -87,6 +87,7 @@ p, *, *, GET, /api/get-prometheus-info, *, *
p, *, *, *, /api/metrics, *, * p, *, *, *, /api/metrics, *, *
p, *, *, GET, /api/get-pricing, *, * p, *, *, GET, /api/get-pricing, *, *
p, *, *, GET, /api/get-plan, *, * p, *, *, GET, /api/get-plan, *, *
p, *, *, GET, /api/get-subscription, *, *
p, *, *, GET, /api/get-organization-names, *, * p, *, *, GET, /api/get-organization-names, *, *
` `

View File

@@ -140,25 +140,28 @@ func (c *ApiController) Signup() {
username = id username = id
} }
password := authForm.Password
msg = object.CheckPasswordComplexityByOrg(organization, password)
if msg != "" {
c.ResponseError(msg)
return
}
initScore, err := organization.GetInitScore() initScore, err := organization.GetInitScore()
if err != nil { if err != nil {
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error()) c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
return return
} }
userType := "normal-user"
if authForm.Plan != "" && authForm.Pricing != "" {
err = object.CheckPricingAndPlan(authForm.Organization, authForm.Pricing, authForm.Plan)
if err != nil {
c.ResponseError(err.Error())
return
}
userType = "paid-user"
}
user := &object.User{ user := &object.User{
Owner: authForm.Organization, Owner: authForm.Organization,
Name: username, Name: username,
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
Id: id, Id: id,
Type: "normal-user", Type: userType,
Password: authForm.Password, Password: authForm.Password,
DisplayName: authForm.Name, DisplayName: authForm.Name,
Avatar: organization.DefaultAvatar, Avatar: organization.DefaultAvatar,
@@ -210,7 +213,7 @@ func (c *ApiController) Signup() {
return return
} }
if application.HasPromptPage() { if application.HasPromptPage() && user.Type == "normal-user" {
// The prompt page needs the user to be signed in // The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId()) c.SetSessionUsername(user.GetId())
} }
@@ -227,15 +230,6 @@ func (c *ApiController) Signup() {
return return
} }
isSignupFromPricing := authForm.Plan != "" && authForm.Pricing != ""
if isSignupFromPricing {
_, err = object.Subscribe(organization.Name, user.Name, authForm.Plan, authForm.Pricing)
if err != nil {
c.ResponseError(err.Error())
return
}
}
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name

View File

@@ -62,13 +62,13 @@ func (c *ApiController) GetApplications() {
} }
paginator := pagination.SetPaginator(c.Ctx, limit, count) paginator := pagination.SetPaginator(c.Ctx, limit, count)
app, err := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder) application, err := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
applications := object.GetMaskedApplications(app, userId) applications := object.GetMaskedApplications(application, userId)
c.ResponseOk(applications, paginator.Nums()) c.ResponseOk(applications, paginator.Nums())
} }
} }
@@ -84,13 +84,23 @@ func (c *ApiController) GetApplication() {
userId := c.GetSessionUsername() userId := c.GetSessionUsername()
id := c.Input().Get("id") id := c.Input().Get("id")
app, err := object.GetApplication(id) application, err := object.GetApplication(id)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk(object.GetMaskedApplication(app, userId)) if c.Input().Get("withKey") != "" && application.Cert != "" {
cert, err := object.GetCert(util.GetId(application.Owner, application.Cert))
if err != nil {
c.ResponseError(err.Error())
return
}
application.CertPublicKey = cert.Certificate
}
c.ResponseOk(object.GetMaskedApplication(application, userId))
} }
// GetUserApplication // GetUserApplication
@@ -164,13 +174,13 @@ func (c *ApiController) GetOrganizationApplications() {
} }
paginator := pagination.SetPaginator(c.Ctx, limit, count) paginator := pagination.SetPaginator(c.Ctx, limit, count)
app, err := object.GetPaginationOrganizationApplications(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder) application, err := object.GetPaginationOrganizationApplications(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
applications := object.GetMaskedApplications(app, userId) applications := object.GetMaskedApplications(application, userId)
c.ResponseOk(applications, paginator.Nums()) c.ResponseOk(applications, paginator.Nums())
} }
} }

View File

@@ -78,6 +78,46 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
} }
} }
// check whether paid-user have active subscription
if user.Type == "paid-user" {
subscriptions, err := object.GetSubscriptionsByUser(user.Owner, user.Name)
if err != nil {
c.ResponseError(err.Error())
return
}
existActiveSubscription := false
for _, subscription := range subscriptions {
if subscription.State == object.SubStateActive {
existActiveSubscription = true
break
}
}
if !existActiveSubscription {
// check pending subscription
for _, sub := range subscriptions {
if sub.State == object.SubStatePending {
c.ResponseOk("BuyPlanResult", sub)
return
}
}
// paid-user does not have active or pending subscription, find the default pricing of application
pricing, err := object.GetApplicationDefaultPricing(application.Organization, application.Name)
if err != nil {
c.ResponseError(err.Error())
return
}
if pricing == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"), user.Name, application.Name))
return
} else {
// let the paid-user select plan
c.ResponseOk("SelectPlan", pricing)
return
}
}
}
if form.Type == ResponseTypeLogin { if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId) c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId) util.LogInfo(c.Ctx, "API: [%s] signed in", userId)

View File

@@ -61,6 +61,10 @@ func (c *ApiController) IsAdminOrSelf(user2 *object.User) bool {
return true return true
} }
if user == nil || user2 == nil {
return false
}
if user.Owner == user2.Owner && user.Name == user2.Name { if user.Owner == user2.Owner && user.Name == user2.Name {
return true return true
} }

View File

@@ -35,6 +35,11 @@ const (
UnauthorizedService string = "UNAUTHORIZED_SERVICE" UnauthorizedService string = "UNAUTHORIZED_SERVICE"
) )
func queryUnescape(service string) string {
s, _ := url.QueryUnescape(service)
return s
}
func (c *RootController) CasValidate() { func (c *RootController) CasValidate() {
ticket := c.Input().Get("ticket") ticket := c.Input().Get("ticket")
service := c.Input().Get("service") service := c.Input().Get("service")
@@ -60,24 +65,25 @@ func (c *RootController) CasServiceValidate() {
if !strings.HasPrefix(ticket, "ST") { if !strings.HasPrefix(ticket, "ST") {
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format) c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
} }
c.CasP3ServiceAndProxyValidate() c.CasP3ProxyValidate()
} }
func (c *RootController) CasProxyValidate() { func (c *RootController) CasProxyValidate() {
// https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-Specification.html#26-proxyvalidate-cas-20
// "/proxyValidate" should accept both service tickets and proxy tickets.
c.CasP3ProxyValidate()
}
func (c *RootController) CasP3ServiceValidate() {
ticket := c.Input().Get("ticket") ticket := c.Input().Get("ticket")
format := c.Input().Get("format") format := c.Input().Get("format")
if !strings.HasPrefix(ticket, "PT") { if !strings.HasPrefix(ticket, "ST") {
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format) c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
} }
c.CasP3ServiceAndProxyValidate() c.CasP3ProxyValidate()
} }
func queryUnescape(service string) string { func (c *RootController) CasP3ProxyValidate() {
s, _ := url.QueryUnescape(service)
return s
}
func (c *RootController) CasP3ServiceAndProxyValidate() {
ticket := c.Input().Get("ticket") ticket := c.Input().Get("ticket")
format := c.Input().Get("format") format := c.Input().Get("format")
service := c.Input().Get("service") service := c.Input().Get("service")
@@ -115,15 +121,17 @@ func (c *RootController) CasP3ServiceAndProxyValidate() {
pgtiou := serviceResponse.Success.ProxyGrantingTicket pgtiou := serviceResponse.Success.ProxyGrantingTicket
// todo: check whether it is https // todo: check whether it is https
pgtUrlObj, err := url.Parse(pgtUrl) pgtUrlObj, err := url.Parse(pgtUrl)
if err != nil {
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, err.Error(), format)
return
}
if pgtUrlObj.Scheme != "https" { if pgtUrlObj.Scheme != "https" {
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, "callback is not https", format) c.sendCasAuthenticationResponseErr(InvalidProxyCallback, "callback is not https", format)
return return
} }
// make a request to pgturl passing pgt and pgtiou // make a request to pgturl passing pgt and pgtiou
if err != nil {
c.sendCasAuthenticationResponseErr(InternalError, err.Error(), format)
return
}
param := pgtUrlObj.Query() param := pgtUrlObj.Query()
param.Add("pgtId", pgt) param.Add("pgtId", pgt)
param.Add("pgtIou", pgtiou) param.Add("pgtIou", pgtiou)
@@ -263,7 +271,6 @@ func (c *RootController) sendCasAuthenticationResponseErr(code, msg, format stri
Message: msg, Message: msg,
}, },
} }
if format == "json" { if format == "json" {
c.Data["json"] = serviceResponse c.Data["json"] = serviceResponse
c.ServeJSON() c.ServeJSON()

View File

@@ -83,7 +83,7 @@ func (c *ApiController) GetEnforcer() {
return return
} }
if loadModelCfg == "true" { if loadModelCfg == "true" && enforcer.Model != "" {
err := enforcer.LoadModelCfg() err := enforcer.LoadModelCfg()
if err != nil { if err != nil {
return return

View File

@@ -16,6 +16,7 @@ package controllers
import ( import (
"fmt" "fmt"
"os"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@@ -32,16 +33,15 @@ func (c *ApiController) UploadPermissions() {
} }
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename)) fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId) path := util.GetUploadXlsxPath(fileId)
util.EnsureFileFolderExists(path) defer os.Remove(path)
err = saveFile(path, &file) err = saveFile(path, &file)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
affected, err := object.UploadPermissions(owner, fileId) affected, err := object.UploadPermissions(owner, path)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} }

View File

@@ -16,6 +16,7 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@@ -82,7 +83,10 @@ func (c *ApiController) GetPlan() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if plan == nil {
c.ResponseError(fmt.Sprintf(c.T("plan:The plan: %s does not exist"), id))
return
}
if includeOption { if includeOption {
options, err := object.GetPermissionsByRole(plan.Role) options, err := object.GetPermissionsByRole(plan.Role)
if err != nil { if err != nil {
@@ -110,14 +114,29 @@ func (c *ApiController) GetPlan() {
// @router /update-plan [post] // @router /update-plan [post]
func (c *ApiController) UpdatePlan() { func (c *ApiController) UpdatePlan() {
id := c.Input().Get("id") id := c.Input().Get("id")
owner := util.GetOwnerFromId(id)
var plan object.Plan var plan object.Plan
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan) err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if plan.Product != "" {
productId := util.GetId(owner, plan.Product)
product, err := object.GetProduct(productId)
if err != nil {
c.ResponseError(err.Error())
return
}
if product != nil {
object.UpdateProductForPlan(&plan, product)
_, err = object.UpdateProduct(productId, product)
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan)) c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
c.ServeJSON() c.ServeJSON()
} }
@@ -136,7 +155,14 @@ func (c *ApiController) AddPlan() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
// Create a related product for plan
product := object.CreateProductForPlan(&plan)
_, err = object.AddProduct(product)
if err != nil {
c.ResponseError(err.Error())
return
}
plan.Product = product.Name
c.Data["json"] = wrapActionResponse(object.AddPlan(&plan)) c.Data["json"] = wrapActionResponse(object.AddPlan(&plan))
c.ServeJSON() c.ServeJSON()
} }
@@ -155,7 +181,13 @@ func (c *ApiController) DeletePlan() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if plan.Product != "" {
_, err = object.DeleteProduct(&object.Product{Owner: plan.Owner, Name: plan.Product})
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.Data["json"] = wrapActionResponse(object.DeletePlan(&plan)) c.Data["json"] = wrapActionResponse(object.DeletePlan(&plan))
c.ServeJSON() c.ServeJSON()
} }

View File

@@ -16,6 +16,7 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@@ -80,7 +81,10 @@ func (c *ApiController) GetPricing() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if pricing == nil {
c.ResponseError(fmt.Sprintf(c.T("pricing:The pricing: %s does not exist"), id))
return
}
c.ResponseOk(pricing) c.ResponseOk(pricing)
} }

View File

@@ -161,10 +161,17 @@ func (c *ApiController) DeleteProduct() {
// @router /buy-product [post] // @router /buy-product [post]
func (c *ApiController) BuyProduct() { func (c *ApiController) BuyProduct() {
id := c.Input().Get("id") id := c.Input().Get("id")
providerName := c.Input().Get("providerName")
host := c.Ctx.Request.Host host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName")
userId := c.GetSessionUsername() // buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
paidUserName := c.Input().Get("userName")
owner, _ := util.GetOwnerAndNameFromId(id)
userId := util.GetId(owner, paidUserName)
if paidUserName == "" {
userId = c.GetSessionUsername()
}
if userId == "" { if userId == "" {
c.ResponseError(c.T("general:Please login first")) c.ResponseError(c.T("general:Please login first"))
return return
@@ -175,13 +182,12 @@ func (c *ApiController) BuyProduct() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if user == nil { if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId)) c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
return return
} }
payUrl, orderId, err := object.BuyProduct(id, providerName, user, host) payUrl, orderId, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@@ -16,6 +16,7 @@ package controllers
import ( import (
"fmt" "fmt"
"os"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@@ -32,16 +33,15 @@ func (c *ApiController) UploadRoles() {
} }
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename)) fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId) path := util.GetUploadXlsxPath(fileId)
util.EnsureFileFolderExists(path) defer os.Remove(path)
err = saveFile(path, &file) err = saveFile(path, &file)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
affected, err := object.UploadRoles(owner, fileId) affected, err := object.UploadRoles(owner, path)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} }

View File

@@ -160,7 +160,11 @@ func (c *ApiController) RunSyncer() {
return return
} }
object.RunSyncer(syncer) err = object.RunSyncer(syncer)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk() c.ResponseOk()
} }

View File

@@ -47,19 +47,16 @@ func (c *ApiController) GetSystemInfo() {
// @router /get-version-info [get] // @router /get-version-info [get]
func (c *ApiController) GetVersionInfo() { func (c *ApiController) GetVersionInfo() {
versionInfo, err := util.GetVersionInfo() versionInfo, err := util.GetVersionInfo()
if versionInfo.Version != "" {
c.ResponseOk(versionInfo)
return
}
versionInfo, err = util.GetVersionInfoFromFile()
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if versionInfo.Version == "" {
versionInfo, err = util.GetVersionInfoFromFile()
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.ResponseOk(versionInfo) c.ResponseOk(versionInfo)
} }

View File

@@ -48,17 +48,17 @@ func (c *ApiController) UploadUsers() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId) path := util.GetUploadXlsxPath(fileId)
util.EnsureFileFolderExists(path) defer os.Remove(path)
err = saveFile(path, &file) err = saveFile(path, &file)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
affected, err := object.UploadUsers(owner, fileId) affected, err := object.UploadUsers(owner, path)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@@ -151,7 +151,7 @@ func (adapter *Adapter) InitAdapter() error {
if adapter.Adapter == nil { if adapter.Adapter == nil {
var dataSourceName string var dataSourceName string
if adapter.builtInAdapter() { if adapter.isBuiltIn() {
dataSourceName = conf.GetConfigString("dataSourceName") dataSourceName = conf.GetConfigString("dataSourceName")
if adapter.DatabaseType == "mysql" { if adapter.DatabaseType == "mysql" {
dataSourceName = dataSourceName + adapter.Database dataSourceName = dataSourceName + adapter.Database
@@ -183,6 +183,14 @@ func (adapter *Adapter) InitAdapter() error {
var err error var err error
engine, err := xorm.NewEngine(adapter.DatabaseType, dataSourceName) engine, err := xorm.NewEngine(adapter.DatabaseType, dataSourceName)
if adapter.isBuiltIn() && adapter.DatabaseType == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
engine.SetSchema(schema)
}
}
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, adapter.getTable(), adapter.TableNamePrefix) adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, adapter.getTable(), adapter.TableNamePrefix)
if err != nil { if err != nil {
return err return err
@@ -211,7 +219,7 @@ func adapterChangeTrigger(oldName string, newName string) error {
return session.Commit() return session.Commit()
} }
func (adapter *Adapter) builtInAdapter() bool { func (adapter *Adapter) isBuiltIn() bool {
if adapter.Owner != "built-in" { if adapter.Owner != "built-in" {
return false return false
} }

View File

@@ -57,6 +57,7 @@ type Application struct {
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
CertPublicKey string `xorm:"-" json:"certPublicKey"`
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"` InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"`

View File

@@ -66,8 +66,11 @@ func CheckUserSignup(application *Application, organization *Organization, form
} }
} }
if len(form.Password) <= 5 { if application.IsSignupItemVisible("Password") {
return i18n.Translate(lang, "check:Password must have at least 6 characters") msg := CheckPasswordComplexityByOrg(organization, form.Password)
if msg != "" {
return msg
}
} }
if application.IsSignupItemVisible("Email") { if application.IsSignupItemVisible("Email") {
@@ -126,7 +129,9 @@ func CheckUserSignup(application *Application, organization *Organization, form
if len(application.InvitationCodes) > 0 { if len(application.InvitationCodes) > 0 {
if form.InvitationCode == "" { if form.InvitationCode == "" {
return i18n.Translate(lang, "check:Invitation code cannot be blank") if application.IsSignupItemRequired("Invitation code") {
return i18n.Translate(lang, "check:Invitation code cannot be blank")
}
} else { } else {
if !util.InSlice(application.InvitationCodes, form.InvitationCode) { if !util.InSlice(application.InvitationCodes, form.InvitationCode) {
return i18n.Translate(lang, "check:Invitation code is invalid") return i18n.Translate(lang, "check:Invitation code is invalid")
@@ -414,7 +419,7 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
} }
} }
if oldUser.Email != user.Email { if oldUser.Email != user.Email {
if HasUserByField(user.Name, "email", user.Email) { if HasUserByField(user.Owner, "email", user.Email) {
return i18n.Translate(lang, "check:Email already exists") return i18n.Translate(lang, "check:Email already exists")
} }
} }

View File

@@ -18,11 +18,14 @@ import (
"database/sql" "database/sql"
"flag" "flag"
"fmt" "fmt"
"os"
"regexp"
"runtime" "runtime"
"strings" "strings"
"github.com/beego/beego" "github.com/beego/beego"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
xormadapter "github.com/casdoor/xorm-adapter/v3" xormadapter "github.com/casdoor/xorm-adapter/v3"
_ "github.com/denisenkom/go-mssqldb" // db = mssql _ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql _ "github.com/go-sql-driver/mysql" // db = mysql
@@ -65,6 +68,17 @@ func InitConfig() {
} }
func InitAdapter() { func InitAdapter() {
if conf.GetConfigString("driverName") == "" {
if !util.FileExist("conf/app.conf") {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
dir = strings.ReplaceAll(dir, "\\", "/")
panic(fmt.Sprintf("The Casdoor config file: \"app.conf\" was not found, it should be placed at: \"%s/conf/app.conf\"", dir))
}
}
if createDatabase { if createDatabase {
err := createDatabaseForPostgres(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName")) err := createDatabaseForPostgres(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
if err != nil { if err != nil {
@@ -122,9 +136,14 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) *Ormer
return a return a
} }
func refineDataSourceNameForPostgres(dataSourceName string) string {
reg := regexp.MustCompile(`dbname=[^ ]+\s*`)
return reg.ReplaceAllString(dataSourceName, "")
}
func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error { func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error {
if driverName == "postgres" { if driverName == "postgres" {
db, err := sql.Open(driverName, dataSourceName) db, err := sql.Open(driverName, refineDataSourceNameForPostgres(dataSourceName))
if err != nil { if err != nil {
return err return err
} }
@@ -136,6 +155,21 @@ func createDatabaseForPostgres(driverName string, dataSourceName string, dbName
return err return err
} }
} }
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
db, err = sql.Open(driverName, dataSourceName)
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s;", schema))
if err != nil {
if !strings.Contains(err.Error(), "already exists") {
return err
}
}
}
return nil return nil
} else { } else {
@@ -168,6 +202,12 @@ func (a *Ormer) open() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
if a.driverName == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
engine.SetSchema(schema)
}
}
a.Engine = engine a.Engine = engine
} }

View File

@@ -79,9 +79,7 @@ func (p *Permission) setEnforcerAdapter(enforcer *casbin.Enforcer) error {
} }
} }
tableNamePrefix := conf.GetConfigString("tableNamePrefix") tableNamePrefix := conf.GetConfigString("tableNamePrefix")
driverName := conf.GetConfigString("driverName") adapter, err := xormadapter.NewAdapterByEngineWithTableName(ormer.Engine, tableName, tableNamePrefix)
dataSourceName := conf.GetConfigRealDataSourceName(driverName)
adapter, err := xormadapter.NewAdapterWithTableName(driverName, dataSourceName, tableName, tableNamePrefix, true)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -33,8 +33,8 @@ func getPermissionMap(owner string) (map[string]*Permission, error) {
return m, err return m, err
} }
func UploadPermissions(owner string, fileId string) (bool, error) { func UploadPermissions(owner string, path string) (bool, error) {
table := xlsx.ReadXlsxFile(fileId) table := xlsx.ReadXlsxFile(path)
oldUserMap, err := getPermissionMap(owner) oldUserMap, err := getPermissionMap(owner)
if err != nil { if err != nil {

View File

@@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"time"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@@ -28,15 +29,39 @@ type Plan struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Description string `xorm:"varchar(100)" json:"description"` Description string `xorm:"varchar(100)" json:"description"`
PricePerMonth float64 `json:"pricePerMonth"` Price float64 `json:"price"`
PricePerYear float64 `json:"pricePerYear"` Currency string `xorm:"varchar(100)" json:"currency"`
Currency string `xorm:"varchar(100)" json:"currency"` Period string `xorm:"varchar(100)" json:"period"`
IsEnabled bool `json:"isEnabled"` Product string `xorm:"varchar(100)" json:"product"`
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
IsEnabled bool `json:"isEnabled"`
Role string `xorm:"varchar(100)" json:"role"` Role string `xorm:"varchar(100)" json:"role"`
Options []string `xorm:"-" json:"options"` Options []string `xorm:"-" json:"options"`
} }
const (
PeriodMonthly = "Monthly"
PeriodYearly = "Yearly"
)
func (plan *Plan) GetId() string {
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
}
func GetDuration(period string) (startTime time.Time, endTime time.Time) {
if period == PeriodYearly {
startTime = time.Now()
endTime = startTime.AddDate(1, 0, 0)
} else if period == PeriodMonthly {
startTime = time.Now()
endTime = startTime.AddDate(0, 1, 0)
} else {
panic(fmt.Sprintf("invalid period: %s", period))
}
return
}
func GetPlanCount(owner, field, value string) (int64, error) { func GetPlanCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Plan{}) return session.Count(&Plan{})
@@ -114,38 +139,3 @@ func DeletePlan(plan *Plan) (bool, error) {
} }
return affected != 0, nil return affected != 0, nil
} }
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, error) {
selectedPricing, err := GetPricing(fmt.Sprintf("%s/%s", owner, pricing))
if err != nil {
return nil, err
}
valid := selectedPricing != nil && selectedPricing.IsEnabled
if !valid {
return nil, nil
}
planBelongToPricing, err := selectedPricing.HasPlan(owner, plan)
if err != nil {
return nil, err
}
if planBelongToPricing {
newSubscription := NewSubscription(owner, user, plan, selectedPricing.TrialDuration)
affected, err := AddSubscription(newSubscription)
if err != nil {
return nil, err
}
if affected {
return newSubscription, nil
}
}
return nil, nil
}

View File

@@ -16,7 +16,6 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@@ -33,12 +32,26 @@ type Pricing struct {
IsEnabled bool `json:"isEnabled"` IsEnabled bool `json:"isEnabled"`
TrialDuration int `json:"trialDuration"` TrialDuration int `json:"trialDuration"`
Application string `xorm:"varchar(100)" json:"application"` Application string `xorm:"varchar(100)" json:"application"`
}
Submitter string `xorm:"varchar(100)" json:"submitter"` func (pricing *Pricing) GetId() string {
Approver string `xorm:"varchar(100)" json:"approver"` return fmt.Sprintf("%s/%s", pricing.Owner, pricing.Name)
ApproveTime string `xorm:"varchar(100)" json:"approveTime"` }
State string `xorm:"varchar(100)" json:"state"` func (pricing *Pricing) HasPlan(planName string) (bool, error) {
planId := util.GetId(pricing.Owner, planName)
plan, err := GetPlan(planId)
if err != nil {
return false, err
}
if plan == nil {
return false, fmt.Errorf("plan: %s does not exist", planId)
}
if util.InSlice(pricing.Plans, plan.Name) {
return true, nil
}
return false, nil
} }
func GetPricingCount(owner, field, value string) (int64, error) { func GetPricingCount(owner, field, value string) (int64, error) {
@@ -74,7 +87,7 @@ func getPricing(owner, name string) (*Pricing, error) {
pricing := Pricing{Owner: owner, Name: name} pricing := Pricing{Owner: owner, Name: name}
existed, err := ormer.Engine.Get(&pricing) existed, err := ormer.Engine.Get(&pricing)
if err != nil { if err != nil {
return &pricing, err return nil, err
} }
if existed { if existed {
return &pricing, nil return &pricing, nil
@@ -88,6 +101,20 @@ func GetPricing(id string) (*Pricing, error) {
return getPricing(owner, name) return getPricing(owner, name)
} }
func GetApplicationDefaultPricing(owner, appName string) (*Pricing, error) {
pricings := make([]*Pricing, 0, 1)
err := ormer.Engine.Asc("created_time").Find(&pricings, &Pricing{Owner: owner, Application: appName})
if err != nil {
return nil, err
}
for _, pricing := range pricings {
if pricing.IsEnabled {
return pricing, nil
}
}
return nil, nil
}
func UpdatePricing(id string, pricing *Pricing) (bool, error) { func UpdatePricing(id string, pricing *Pricing) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if p, err := getPricing(owner, name); err != nil { if p, err := getPricing(owner, name); err != nil {
@@ -120,28 +147,21 @@ func DeletePricing(pricing *Pricing) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func (pricing *Pricing) GetId() string { func CheckPricingAndPlan(owner, pricingName, planName string) error {
return fmt.Sprintf("%s/%s", pricing.Owner, pricing.Name) pricingId := util.GetId(owner, pricingName)
} pricing, err := GetPricing(pricingId)
if pricing == nil || err != nil {
func (pricing *Pricing) HasPlan(owner string, plan string) (bool, error) { if pricing == nil && err == nil {
selectedPlan, err := GetPlan(fmt.Sprintf("%s/%s", owner, plan)) err = fmt.Errorf("pricing: %s does not exist", pricingName)
if err != nil {
return false, err
}
if selectedPlan == nil {
return false, nil
}
result := false
for _, pricingPlan := range pricing.Plans {
if strings.Contains(pricingPlan, selectedPlan.Name) {
result = true
break
} }
return err
} }
ok, err := pricing.HasPlan(planName)
return result, nil if err != nil {
return err
}
if !ok {
return fmt.Errorf("pricing: %s does not have plan: %s", pricingName, planName)
}
return nil
} }

View File

@@ -141,24 +141,24 @@ func (product *Product) isValidProvider(provider *Provider) bool {
return false return false
} }
func (product *Product) getProvider(providerId string) (*Provider, error) { func (product *Product) getProvider(providerName string) (*Provider, error) {
provider, err := getProvider(product.Owner, providerId) provider, err := getProvider(product.Owner, providerName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if provider == nil { if provider == nil {
return nil, fmt.Errorf("the payment provider: %s does not exist", providerId) return nil, fmt.Errorf("the payment provider: %s does not exist", providerName)
} }
if !product.isValidProvider(provider) { if !product.isValidProvider(provider) {
return nil, fmt.Errorf("the payment provider: %s is not valid for the product: %s", providerId, product.Name) return nil, fmt.Errorf("the payment provider: %s is not valid for the product: %s", providerName, product.Name)
} }
return provider, nil return provider, nil
} }
func BuyProduct(id string, providerName string, user *User, host string) (string, string, error) { func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (string, string, error) {
product, err := GetProduct(id) product, err := GetProduct(id)
if err != nil { if err != nil {
return "", "", err return "", "", err
@@ -181,13 +181,31 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
owner := product.Owner owner := product.Owner
productName := product.Name productName := product.Name
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName) payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
paymentName := util.GenerateTimeId() paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
productDisplayName := product.DisplayName productDisplayName := product.DisplayName
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName) returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName) notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
// Create an Order and get the payUrl if user.Type == "paid-user" {
// Create a subscription for `paid-user`
if pricingName != "" && planName != "" {
plan, err := GetPlan(util.GetId(owner, planName))
if err != nil {
return "", "", err
}
if plan == nil {
return "", "", fmt.Errorf("the plan: %s does not exist", planName)
}
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
_, err = AddSubscription(sub)
if err != nil {
return "", "", err
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
}
// Create an OrderId and get the payUrl
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl) payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil { if err != nil {
return "", "", err return "", "", err
@@ -228,7 +246,6 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
if !affected { if !affected {
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment)) return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
} }
return payUrl, orderId, err return payUrl, orderId, err
} }
@@ -252,3 +269,38 @@ func ExtendProductWithProviders(product *Product) error {
return nil return nil
} }
func CreateProductForPlan(plan *Plan) *Product {
product := &Product{
Owner: plan.Owner,
Name: fmt.Sprintf("product_%v", util.GetRandomName()),
DisplayName: fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period),
CreatedTime: plan.CreatedTime,
Image: "https://cdn.casbin.org/img/casdoor-logo_1185x256.png", // TODO
Detail: fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period),
Description: plan.Description,
Tag: "auto_created_product_for_plan",
Price: plan.Price,
Currency: plan.Currency,
Quantity: 999,
Sold: 0,
Providers: plan.PaymentProviders,
State: "Published",
}
if product.Providers == nil {
product.Providers = []string{}
}
return product
}
func UpdateProductForPlan(plan *Plan, product *Product) {
product.Owner = plan.Owner
product.DisplayName = fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period)
product.Detail = fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period)
product.Price = plan.Price
product.Currency = plan.Currency
product.Providers = plan.PaymentProviders
}

View File

@@ -33,8 +33,8 @@ func getRoleMap(owner string) (map[string]*Role, error) {
return m, nil return m, nil
} }
func UploadRoles(owner string, fileId string) (bool, error) { func UploadRoles(owner string, path string) (bool, error) {
table := xlsx.ReadXlsxFile(fileId) table := xlsx.ReadXlsxFile(path)
oldUserMap, err := getRoleMap(owner) oldUserMap, err := getRoleMap(owner)
if err != nil { if err != nil {

View File

@@ -18,47 +18,108 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
const defaultStatus = "Pending" type SubscriptionState string
const (
SubStatePending SubscriptionState = "Pending"
SubStateError SubscriptionState = "Error"
SubStateSuspended SubscriptionState = "Suspended" // suspended by the admin
SubStateActive SubscriptionState = "Active"
SubStateUpcoming SubscriptionState = "Upcoming"
SubStateExpired SubscriptionState = "Expired"
)
type Subscription struct { type Subscription struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Description string `xorm:"varchar(100)" json:"description"`
StartDate time.Time `json:"startDate"` User string `xorm:"varchar(100)" json:"user"`
EndDate time.Time `json:"endDate"` Pricing string `xorm:"varchar(100)" json:"pricing"`
Duration int `json:"duration"` Plan string `xorm:"varchar(100)" json:"plan"`
Description string `xorm:"varchar(100)" json:"description"` Payment string `xorm:"varchar(100)" json:"payment"`
User string `xorm:"mediumtext" json:"user"` StartTime time.Time `json:"startTime"`
Plan string `xorm:"varchar(100)" json:"plan"` EndTime time.Time `json:"endTime"`
Period string `xorm:"varchar(100)" json:"period"`
IsEnabled bool `json:"isEnabled"` State SubscriptionState `xorm:"varchar(100)" json:"state"`
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 { func (sub *Subscription) GetId() string {
return fmt.Sprintf("%s/%s", sub.Owner, sub.Name)
}
func (sub *Subscription) UpdateState() error {
preState := sub.State
// update subscription state by payment state
if sub.State == SubStatePending {
if sub.Payment == "" {
return nil
}
payment, err := GetPayment(util.GetId(sub.Owner, sub.Payment))
if err != nil {
return err
}
if payment == nil {
sub.Description = fmt.Sprintf("payment: %s does not exist", sub.Payment)
sub.State = SubStateError
} else {
if payment.State == pp.PaymentStatePaid {
sub.State = SubStateActive
} else if payment.State != pp.PaymentStateCreated {
// other states: Canceled, Timeout, Error
sub.Description = fmt.Sprintf("payment: %s state is %v", sub.Payment, payment.State)
sub.State = SubStateError
}
}
}
if sub.State == SubStateActive || sub.State == SubStateUpcoming || sub.State == SubStateExpired {
if sub.EndTime.Before(time.Now()) {
sub.State = SubStateExpired
} else if sub.StartTime.After(time.Now()) {
sub.State = SubStateUpcoming
} else {
sub.State = SubStateActive
}
}
if preState != sub.State {
_, err := UpdateSubscription(sub.GetId(), sub)
if err != nil {
return err
}
}
return nil
}
func NewSubscription(owner, userName, planName, paymentName, period string) *Subscription {
startTime, endTime := GetDuration(period)
id := util.GenerateId()[:6] id := util.GenerateId()[:6]
return &Subscription{ return &Subscription{
Name: "Subscription_" + id,
DisplayName: "New Subscription - " + id,
Owner: owner, Owner: owner,
User: owner + "/" + user, Name: "sub_" + id,
Plan: owner + "/" + plan, DisplayName: "New Subscription - " + id,
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
State: defaultStatus,
Duration: duration, User: userName,
StartDate: time.Now(), Plan: planName,
EndDate: time.Now().AddDate(0, 0, duration), Payment: paymentName,
StartTime: startTime,
EndTime: endTime,
Period: period,
State: SubStatePending, // waiting for payment complete
} }
} }
@@ -73,7 +134,28 @@ func GetSubscriptions(owner string) ([]*Subscription, error) {
if err != nil { if err != nil {
return subscriptions, err return subscriptions, err
} }
for _, sub := range subscriptions {
err = sub.UpdateState()
if err != nil {
return nil, err
}
}
return subscriptions, nil
}
func GetSubscriptionsByUser(owner, userName string) ([]*Subscription, error) {
subscriptions := []*Subscription{}
err := ormer.Engine.Desc("created_time").Find(&subscriptions, &Subscription{Owner: owner, User: userName})
if err != nil {
return subscriptions, err
}
// update subscription state
for _, sub := range subscriptions {
err = sub.UpdateState()
if err != nil {
return subscriptions, err
}
}
return subscriptions, nil return subscriptions, nil
} }
@@ -84,7 +166,12 @@ func GetPaginationSubscriptions(owner string, offset, limit int, field, value, s
if err != nil { if err != nil {
return subscriptions, err return subscriptions, err
} }
for _, sub := range subscriptions {
err = sub.UpdateState()
if err != nil {
return nil, err
}
}
return subscriptions, nil return subscriptions, nil
} }
@@ -144,7 +231,3 @@ func DeleteSubscription(subscription *Subscription) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func (subscription *Subscription) GetId() string {
return fmt.Sprintf("%s/%s", subscription.Owner, subscription.Name)
}

View File

@@ -37,12 +37,13 @@ type Syncer struct {
Organization string `xorm:"varchar(100)" json:"organization"` Organization string `xorm:"varchar(100)" json:"organization"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
SslMode string `xorm:"varchar(100)" json:"sslMode"`
Host string `xorm:"varchar(100)" json:"host"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"` User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"` Password string `xorm:"varchar(100)" json:"password"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
Database string `xorm:"varchar(100)" json:"database"` Database string `xorm:"varchar(100)" json:"database"`
Table string `xorm:"varchar(100)" json:"table"` Table string `xorm:"varchar(100)" json:"table"`
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"` TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
@@ -250,7 +251,7 @@ func (syncer *Syncer) getKey() string {
return key return key
} }
func RunSyncer(syncer *Syncer) { func RunSyncer(syncer *Syncer) error {
syncer.initAdapter() syncer.initAdapter()
syncer.syncUsers() return syncer.syncUsers()
} }

View File

@@ -52,11 +52,14 @@ func addSyncerJob(syncer *Syncer) error {
syncer.initAdapter() syncer.initAdapter()
syncer.syncUsers() err := syncer.syncUsers()
if err != nil {
return err
}
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval) schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
cron := getCronMap(syncer.Name) cron := getCronMap(syncer.Name)
_, err := cron.AddFunc(schedule, syncer.syncUsers) _, err = cron.AddFunc(schedule, syncer.syncUsersNoError)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -19,15 +19,15 @@ import (
"time" "time"
) )
func (syncer *Syncer) syncUsers() { func (syncer *Syncer) syncUsers() error {
if len(syncer.TableColumns) == 0 { if len(syncer.TableColumns) == 0 {
return return fmt.Errorf("The syncer table columns should not be empty")
} }
fmt.Printf("Running syncUsers()..\n") fmt.Printf("Running syncUsers()..\n")
users, _, _ := syncer.getUserMap() users, _, _ := syncer.getUserMap()
oUsers, oUserMap, err := syncer.getOriginalUserMap() oUsers, _, err := syncer.getOriginalUserMap()
if err != nil { if err != nil {
fmt.Printf(err.Error()) fmt.Printf(err.Error())
@@ -35,10 +35,8 @@ func (syncer *Syncer) syncUsers() {
line := fmt.Sprintf("[%s] %s\n", timestamp, err.Error()) line := fmt.Sprintf("[%s] %s\n", timestamp, err.Error())
_, err = updateSyncerErrorText(syncer, line) _, err = updateSyncerErrorText(syncer, line)
if err != nil { if err != nil {
panic(err) return err
} }
return
} }
fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers)) fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers))
@@ -55,6 +53,11 @@ func (syncer *Syncer) syncUsers() {
myUsers[syncer.getUserValue(m, key)] = m myUsers[syncer.getUserValue(m, key)] = m
} }
myOUsers := map[string]*User{}
for _, m := range oUsers {
myOUsers[syncer.getUserValue(m, key)] = m
}
newUsers := []*User{} newUsers := []*User{}
for _, oUser := range oUsers { for _, oUser := range oUsers {
primary := syncer.getUserValue(oUser, key) primary := syncer.getUserValue(oUser, key)
@@ -71,28 +74,30 @@ func (syncer *Syncer) syncUsers() {
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap) updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
updatedUser.Hash = oHash updatedUser.Hash = oHash
updatedUser.PreHash = oHash updatedUser.PreHash = oHash
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
_, err = syncer.updateUserForOriginalByFields(updatedUser, key) _, err = syncer.updateUserForOriginalByFields(updatedUser, key)
if err != nil { if err != nil {
panic(err) return err
} }
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
} }
} else { } else {
if user.PreHash == oHash { if user.PreHash == oHash {
if !syncer.IsReadOnly { if !syncer.IsReadOnly {
updatedOUser := syncer.createOriginalUserFromUser(user) updatedOUser := syncer.createOriginalUserFromUser(user)
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
_, err = syncer.updateUser(updatedOUser) _, err = syncer.updateUser(updatedOUser)
if err != nil { if err != nil {
panic(err) return err
} }
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
} }
// update preHash // update preHash
user.PreHash = user.Hash user.PreHash = user.Hash
_, err = SetUserField(user, "pre_hash", user.PreHash) _, err = SetUserField(user, "pre_hash", user.PreHash)
if err != nil { if err != nil {
panic(err) return err
} }
} else { } else {
if user.Hash == oHash { if user.Hash == oHash {
@@ -100,17 +105,18 @@ func (syncer *Syncer) syncUsers() {
user.PreHash = user.Hash user.PreHash = user.Hash
_, err = SetUserField(user, "pre_hash", user.PreHash) _, err = SetUserField(user, "pre_hash", user.PreHash)
if err != nil { if err != nil {
panic(err) return err
} }
} else { } else {
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap) updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
updatedUser.Hash = oHash updatedUser.Hash = oHash
updatedUser.PreHash = oHash updatedUser.PreHash = oHash
fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser)
_, err = syncer.updateUserForOriginalByFields(updatedUser, key) _, err = syncer.updateUserForOriginalByFields(updatedUser, key)
if err != nil { if err != nil {
panic(err) return err
} }
fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser)
} }
} }
} }
@@ -118,20 +124,30 @@ func (syncer *Syncer) syncUsers() {
} }
_, err = AddUsersInBatch(newUsers) _, err = AddUsersInBatch(newUsers)
if err != nil { if err != nil {
panic(err) return err
} }
if !syncer.IsReadOnly { if !syncer.IsReadOnly {
for _, user := range users { for _, user := range users {
id := user.Id primary := syncer.getUserValue(user, key)
if _, ok := oUserMap[id]; !ok { if _, ok := myOUsers[primary]; !ok {
newOUser := syncer.createOriginalUserFromUser(user) newOUser := syncer.createOriginalUserFromUser(user)
fmt.Printf("New oUser: %v\n", newOUser)
_, err = syncer.addUser(newOUser) _, err = syncer.addUser(newOUser)
if err != nil { if err != nil {
panic(err) return err
} }
fmt.Printf("New oUser: %v\n", newOUser)
} }
} }
} }
return nil
}
func (syncer *Syncer) syncUsersNoError() {
err := syncer.syncUsers()
if err != nil {
fmt.Printf("syncUsersNoError() error: %s\n", err.Error())
}
} }

View File

@@ -31,8 +31,8 @@ type Credential struct {
} }
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) { func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
sql := fmt.Sprintf("select * from %s", syncer.getTable()) var results []map[string]string
results, err := syncer.Ormer.Engine.QueryString(sql) err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,19 +64,10 @@ func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*Origina
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) { func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user) m := syncer.getMapFromOriginalUser(user)
keyString, valueString := syncer.getSqlKeyValueStringFromMap(m) affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m)
sql := fmt.Sprintf("insert into %s (%s) values (%s)", syncer.getTable(), keyString, valueString)
res, err := syncer.Ormer.Engine.Exec(sql)
if err != nil { if err != nil {
return false, err return false, err
} }
affected, err := res.RowsAffected()
if err != nil {
return false, err
}
return affected != 0, nil return affected != 0, nil
} }
@@ -93,23 +84,14 @@ func (syncer *Syncer) getCasdoorColumns() []string {
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) { func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
key := syncer.getKey() key := syncer.getKey()
m := syncer.getMapFromOriginalUser(user) m := syncer.getMapFromOriginalUser(user)
pkValue := m[key] pkValue := m[key]
delete(m, key) delete(m, key)
setString := syncer.getSqlSetStringFromMap(m)
sql := fmt.Sprintf("update %s set %s where %s = %s", syncer.getTable(), setString, key, pkValue) affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).ID(pkValue).Update(&m)
res, err := syncer.Ormer.Engine.Exec(sql)
if err != nil { if err != nil {
return false, err return false, err
} }
affected, err := res.RowsAffected()
if err != nil {
return false, err
}
return affected != 0, nil return affected != 0, nil
} }
@@ -185,7 +167,11 @@ func (syncer *Syncer) initAdapter() {
if syncer.DatabaseType == "mssql" { if syncer.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database) dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else if syncer.DatabaseType == "postgres" { } else if syncer.DatabaseType == "postgres" {
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database) sslMode := "disable"
if syncer.SslMode != "" {
sslMode = syncer.SslMode
}
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, sslMode, syncer.Database)
} else { } else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port) dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
} }

View File

@@ -322,19 +322,3 @@ func (syncer *Syncer) getSqlSetStringFromMap(m map[string]string) string {
} }
return strings.Join(tokens, ", ") return strings.Join(tokens, ", ")
} }
func (syncer *Syncer) getSqlKeyValueStringFromMap(m map[string]string) (string, string) {
typeMap := syncer.getTableColumnsTypeMap()
keys := []string{}
values := []string{}
for k, v := range m {
if typeMap[k] == "string" {
v = fmt.Sprintf("'%s'", v)
}
keys = append(keys, k)
values = append(values, v)
}
return strings.Join(keys, ", "), strings.Join(values, ", ")
}

View File

@@ -541,7 +541,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
} }
} }
if isAdmin { if isAdmin {
columns = append(columns, "name", "email", "phone", "country_code") columns = append(columns, "name", "email", "phone", "country_code", "type")
} }
if util.ContainsString(columns, "groups") { if util.ContainsString(columns, "groups") {
@@ -627,12 +627,10 @@ func AddUser(user *User) (bool, error) {
return false, nil return false, nil
} }
if user.PasswordType == "" && organization.PasswordType != "" { if user.PasswordType == "" || user.PasswordType == "plain" {
user.PasswordType = organization.PasswordType user.UpdateUserPassword(organization)
} }
user.UpdateUserPassword(organization)
err = user.UpdateUserHash() err = user.UpdateUserHash()
if err != nil { if err != nil {
return false, err return false, err

View File

@@ -1,6 +1,8 @@
package object package object
import ( import (
"fmt"
"github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/errors" "github.com/casbin/casbin/v2/errors"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@@ -17,11 +19,28 @@ func NewUserGroupEnforcer(enforcer *casbin.Enforcer) *UserGroupEnforcer {
} }
} }
func (e *UserGroupEnforcer) checkModel() error {
if _, ok := e.enforcer.GetModel()["g"]; !ok {
return fmt.Errorf("The Casbin model used by enforcer doesn't support RBAC (\"[role_definition]\" section not found), please use a RBAC enabled Casbin model for the enforcer")
}
return nil
}
func (e *UserGroupEnforcer) AddGroupForUser(user string, group string) (bool, error) { func (e *UserGroupEnforcer) AddGroupForUser(user string, group string) (bool, error) {
err := e.checkModel()
if err != nil {
return false, err
}
return e.enforcer.AddRoleForUser(user, GetGroupWithPrefix(group)) return e.enforcer.AddRoleForUser(user, GetGroupWithPrefix(group))
} }
func (e *UserGroupEnforcer) AddGroupsForUser(user string, groups []string) (bool, error) { func (e *UserGroupEnforcer) AddGroupsForUser(user string, groups []string) (bool, error) {
err := e.checkModel()
if err != nil {
return false, err
}
g := make([]string, len(groups)) g := make([]string, len(groups))
for i, group := range groups { for i, group := range groups {
g[i] = GetGroupWithPrefix(group) g[i] = GetGroupWithPrefix(group)
@@ -30,14 +49,29 @@ func (e *UserGroupEnforcer) AddGroupsForUser(user string, groups []string) (bool
} }
func (e *UserGroupEnforcer) DeleteGroupForUser(user string, group string) (bool, error) { func (e *UserGroupEnforcer) DeleteGroupForUser(user string, group string) (bool, error) {
err := e.checkModel()
if err != nil {
return false, err
}
return e.enforcer.DeleteRoleForUser(user, GetGroupWithPrefix(group)) return e.enforcer.DeleteRoleForUser(user, GetGroupWithPrefix(group))
} }
func (e *UserGroupEnforcer) DeleteGroupsForUser(user string) (bool, error) { func (e *UserGroupEnforcer) DeleteGroupsForUser(user string) (bool, error) {
err := e.checkModel()
if err != nil {
return false, err
}
return e.enforcer.DeleteRolesForUser(user) return e.enforcer.DeleteRolesForUser(user)
} }
func (e *UserGroupEnforcer) GetGroupsForUser(user string) ([]string, error) { func (e *UserGroupEnforcer) GetGroupsForUser(user string) ([]string, error) {
err := e.checkModel()
if err != nil {
return nil, err
}
groups, err := e.enforcer.GetRolesForUser(user) groups, err := e.enforcer.GetRolesForUser(user)
for i, group := range groups { for i, group := range groups {
groups[i] = GetGroupWithoutPrefix(group) groups[i] = GetGroupWithoutPrefix(group)
@@ -46,6 +80,11 @@ func (e *UserGroupEnforcer) GetGroupsForUser(user string) ([]string, error) {
} }
func (e *UserGroupEnforcer) GetAllUsersByGroup(group string) ([]string, error) { func (e *UserGroupEnforcer) GetAllUsersByGroup(group string) ([]string, error) {
err := e.checkModel()
if err != nil {
return nil, err
}
users, err := e.enforcer.GetUsersForRole(GetGroupWithPrefix(group)) users, err := e.enforcer.GetUsersForRole(GetGroupWithPrefix(group))
if err != nil { if err != nil {
if err == errors.ERR_NAME_NOT_FOUND { if err == errors.ERR_NAME_NOT_FOUND {
@@ -65,13 +104,17 @@ func GetGroupWithoutPrefix(group string) string {
} }
func (e *UserGroupEnforcer) GetUserNamesByGroupName(groupName string) ([]string, error) { func (e *UserGroupEnforcer) GetUserNamesByGroupName(groupName string) ([]string, error) {
var names []string err := e.checkModel()
if err != nil {
return nil, err
}
userIds, err := e.GetAllUsersByGroup(groupName) userIds, err := e.GetAllUsersByGroup(groupName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
names := []string{}
for _, userId := range userIds { for _, userId := range userIds {
_, name := util.GetOwnerAndNameFromIdNoCheck(userId) _, name := util.GetOwnerAndNameFromIdNoCheck(userId)
names = append(names, name) names = append(names, name)
@@ -81,7 +124,12 @@ func (e *UserGroupEnforcer) GetUserNamesByGroupName(groupName string) ([]string,
} }
func (e *UserGroupEnforcer) UpdateGroupsForUser(user string, groups []string) (bool, error) { func (e *UserGroupEnforcer) UpdateGroupsForUser(user string, groups []string) (bool, error) {
_, err := e.DeleteGroupsForUser(user) err := e.checkModel()
if err != nil {
return false, err
}
_, err = e.DeleteGroupsForUser(user)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@@ -73,8 +73,8 @@ func parseListItem(lines *[]string, i int) []string {
return trimmedItems return trimmedItems
} }
func UploadUsers(owner string, fileId string) (bool, error) { func UploadUsers(owner string, path string) (bool, error) {
table := xlsx.ReadXlsxFile(fileId) table := xlsx.ReadXlsxFile(path)
oldUserMap, err := getUserMap(owner) oldUserMap, err := getUserMap(owner)
if err != nil { if err != nil {

View File

@@ -14,10 +14,7 @@
package pp package pp
import ( import "net/http"
"fmt"
"net/http"
)
type DummyPaymentProvider struct{} type DummyPaymentProvider struct{}
@@ -27,8 +24,7 @@ func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
} }
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) { func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
payUrl := fmt.Sprintf("/payments/%s/result", paymentName) return returnUrl, "", nil
return payUrl, "", nil
} }
func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {

View File

@@ -114,8 +114,8 @@ func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, auth
if err != nil { if err != nil {
return nil, err return nil, err
} }
if captureRsp.Code != paypal.Success { if detailRsp.Code != paypal.Success {
errDetail := captureRsp.ErrorResponse.Details[0] errDetail := detailRsp.ErrorResponse.Details[0]
switch errDetail.Issue { switch errDetail.Issue {
case "ORDER_NOT_APPROVED": case "ORDER_NOT_APPROVED":
notifyResult.PaymentStatus = PaymentStateCanceled notifyResult.PaymentStatus = PaymentStateCanceled

View File

@@ -14,9 +14,7 @@
package pp package pp
import ( import "net/http"
"net/http"
)
type PaymentState string type PaymentState string

View File

@@ -273,7 +273,7 @@ func initAPI() {
beego.Router("/cas/:organization/:application/proxy", &controllers.RootController{}, "GET:CasProxy") beego.Router("/cas/:organization/:application/proxy", &controllers.RootController{}, "GET:CasProxy")
beego.Router("/cas/:organization/:application/validate", &controllers.RootController{}, "GET:CasValidate") beego.Router("/cas/:organization/:application/validate", &controllers.RootController{}, "GET:CasValidate")
beego.Router("/cas/:organization/:application/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate") beego.Router("/cas/:organization/:application/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceValidate")
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate") beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ProxyValidate")
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate") beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
} }

View File

@@ -34,16 +34,6 @@ func GetPath(path string) string {
return filepath.Dir(path) return filepath.Dir(path)
} }
func EnsureFileFolderExists(path string) {
p := GetPath(path)
if !FileExist(p) {
err := os.MkdirAll(p, os.ModePerm)
if err != nil {
panic(err)
}
}
}
func ListFiles(path string) []string { func ListFiles(path string) []string {
res := []string{} res := []string{}

View File

@@ -14,8 +14,13 @@
package util package util
import "fmt" import "io/ioutil"
func GetUploadXlsxPath(fileId string) string { func GetUploadXlsxPath(fileId string) string {
return fmt.Sprintf("tmpFiles/%s.xlsx", fileId) file, err := ioutil.TempFile("", fileId)
if err != nil {
panic(err)
}
return file.Name()
} }

View File

@@ -1,40 +0,0 @@
// Copyright 2021 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 util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetUploadXlsxPath(t *testing.T) {
scenarios := []struct {
description string
input string
expected interface{}
}{
{"scenery one", "casdoor", "tmpFiles/casdoor.xlsx"},
{"scenery two", "casbin", "tmpFiles/casbin.xlsx"},
{"scenery three", "loremIpsum", "tmpFiles/loremIpsum.xlsx"},
{"scenery four", "", "tmpFiles/.xlsx"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetUploadXlsxPath(scenery.input)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}

View File

@@ -23,6 +23,7 @@ import (
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -315,3 +316,13 @@ func ParseIdToString(input interface{}) (string, error) {
return "", fmt.Errorf("unsupported id type: %T", input) return "", fmt.Errorf("unsupported id type: %T", input)
} }
} }
func GetValueFromDataSourceName(key string, dataSourceName string) string {
reg := regexp.MustCompile(key + "=([^ ]+)")
matches := reg.FindStringSubmatch(dataSourceName)
if len(matches) >= 2 {
return matches[1]
}
return ""
}

View File

@@ -52,7 +52,7 @@
}, },
"scripts": { "scripts": {
"start": "cross-env PORT=7001 craco start", "start": "cross-env PORT=7001 craco start",
"build": "craco --max_old_space_size=4096 build", "build": "craco build",
"test": "craco test", "test": "craco test",
"eject": "craco eject", "eject": "craco eject",
"crowdin:sync": "crowdin upload && crowdin download", "crowdin:sync": "crowdin upload && crowdin download",

View File

@@ -89,6 +89,7 @@ import ThemeSelect from "./common/select/ThemeSelect";
import OrganizationSelect from "./common/select/OrganizationSelect"; import OrganizationSelect from "./common/select/OrganizationSelect";
import {clearWeb3AuthToken} from "./auth/Web3Auth"; import {clearWeb3AuthToken} from "./auth/Web3Auth";
import AccountAvatar from "./account/AccountAvatar"; import AccountAvatar from "./account/AccountAvatar";
import OpenTour from "./common/OpenTour";
const {Header, Footer, Content} = Layout; const {Header, Footer, Content} = Layout;
@@ -379,6 +380,7 @@ class App extends Component {
}); });
}} /> }} />
<LanguageSelect languages={this.state.account.organization.languages} /> <LanguageSelect languages={this.state.account.organization.languages} />
<OpenTour />
{Setting.isAdminUser(this.state.account) && !Setting.isMobile() && {Setting.isAdminUser(this.state.account) && !Setting.isMobile() &&
<OrganizationSelect <OrganizationSelect
initValue={Setting.getOrganization()} initValue={Setting.getOrganization()}
@@ -448,7 +450,7 @@ class App extends Component {
res.push(Setting.getItem(<Link style={{color: "black"}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone />, [ res.push(Setting.getItem(<Link style={{color: "black"}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone />, [
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"), Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
Setting.getItem(<a target="_blank" rel="noreferrer" href={"http://localhost:18001/records"}>{i18next.t("general:Records")}</a>, "/records"), Setting.getItem(<a target="_blank" rel="noreferrer" href={Conf.CasvisorUrl}>{i18next.t("general:Records")}</a>, "/records"),
Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>, "/tokens"), Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>, "/tokens"),
])); ]));
@@ -460,11 +462,17 @@ class App extends Component {
Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>, "/subscriptions"), Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>, "/subscriptions"),
])); ]));
res.push(Setting.getItem(<Link style={{color: "black"}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone />, [ if (Setting.isAdminUser(this.state.account)) {
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"), res.push(Setting.getItem(<Link style={{color: "black"}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone />, [
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"), Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"), Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")])); Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
} else {
res.push(Setting.getItem(<Link style={{color: "black"}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone />, [
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks")]));
}
} }
return res; return res;
@@ -563,9 +571,7 @@ class App extends Component {
renderContent() { renderContent() {
const onClick = ({key}) => { const onClick = ({key}) => {
if (key === "/swagger") { if (key !== "/swagger" && key !== "/records") {
window.open(Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger", "_blank");
} else {
if (this.state.requiredEnableMfa) { if (this.state.requiredEnableMfa) {
Setting.showMessage("info", "Please enable MFA first!"); Setting.showMessage("info", "Please enable MFA first!");
} else { } else {
@@ -657,7 +663,8 @@ class App extends Component {
window.location.pathname.startsWith("/result") || window.location.pathname.startsWith("/result") ||
window.location.pathname.startsWith("/cas") || window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/auto-signup") || window.location.pathname.startsWith("/auto-signup") ||
window.location.pathname.startsWith("/select-plan"); window.location.pathname.startsWith("/select-plan") ||
window.location.pathname.startsWith("/buy-plan");
} }
renderPage() { renderPage() {

View File

@@ -13,11 +13,12 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Input, Result, Space} from "antd"; import {Button, Input, Result, Space, Tour} from "antd";
import {SearchOutlined} from "@ant-design/icons"; import {SearchOutlined} from "@ant-design/icons";
import Highlighter from "react-highlight-words"; import Highlighter from "react-highlight-words";
import i18next from "i18next"; import i18next from "i18next";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as TourConfig from "./TourConfig";
class BaseListPage extends React.Component { class BaseListPage extends React.Component {
constructor(props) { constructor(props) {
@@ -33,6 +34,7 @@ class BaseListPage extends React.Component {
searchText: "", searchText: "",
searchedColumn: "", searchedColumn: "",
isAuthorized: true, isAuthorized: true,
isTourVisible: TourConfig.getTourVisible(),
}; };
} }
@@ -41,14 +43,23 @@ class BaseListPage extends React.Component {
this.fetch({pagination}); this.fetch({pagination});
}; };
handleTourChange = () => {
this.setState({isTourVisible: TourConfig.getTourVisible()});
};
componentDidMount() { componentDidMount() {
window.addEventListener("storageOrganizationChanged", this.handleOrganizationChange); window.addEventListener("storageOrganizationChanged", this.handleOrganizationChange);
window.addEventListener("storageTourChanged", this.handleTourChange);
if (!Setting.isAdminUser(this.props.account)) { if (!Setting.isAdminUser(this.props.account)) {
Setting.setOrganization("All"); Setting.setOrganization("All");
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (this.state.intervalId !== null) {
clearInterval(this.state.intervalId);
}
window.removeEventListener("storageTourChanged", this.handleTourChange);
window.removeEventListener("storageOrganizationChanged", this.handleOrganizationChange); window.removeEventListener("storageOrganizationChanged", this.handleOrganizationChange);
} }
@@ -144,6 +155,37 @@ class BaseListPage extends React.Component {
}); });
}; };
setIsTourVisible = () => {
TourConfig.setIsTourVisible(false);
this.setState({isTourVisible: false});
};
getSteps = () => {
const nextPathName = TourConfig.getNextUrl();
const steps = TourConfig.getSteps();
steps.map((item, index) => {
if (!index) {
item.target = () => document.querySelector("table");
} else {
item.target = () => document.getElementById(item.id) || null;
}
if (index === steps.length - 1) {
item.nextButtonProps = {
children: TourConfig.getNextButtonChild(nextPathName),
};
}
});
return steps;
};
handleTourComplete = () => {
const nextPathName = TourConfig.getNextUrl();
if (nextPathName !== "") {
this.props.history.push("/" + nextPathName);
TourConfig.setIsTourVisible(true);
}
};
render() { render() {
if (!this.state.isAuthorized) { if (!this.state.isAuthorized) {
return ( return (
@@ -161,6 +203,17 @@ class BaseListPage extends React.Component {
{ {
this.renderTable(this.state.data) this.renderTable(this.state.data)
} }
<Tour
open={this.state.isTourVisible}
onClose={this.setIsTourVisible}
steps={this.getSteps()}
indicatorsRender={(current, total) => (
<span>
{current + 1} / {total}
</span>
)}
onFinish={this.handleTourComplete}
/>
</div> </div>
); );
} }

View File

@@ -14,6 +14,8 @@
export const DefaultApplication = "app-built-in"; export const DefaultApplication = "app-built-in";
export const CasvisorUrl = "https://github.com/casbin/casvisor";
export const ShowGithubCorner = false; export const ShowGithubCorner = false;
export const IsDemoMode = false; export const IsDemoMode = false;

View File

@@ -29,6 +29,8 @@ import PromptPage from "./auth/PromptPage";
import ResultPage from "./auth/ResultPage"; import ResultPage from "./auth/ResultPage";
import CasLogout from "./auth/CasLogout"; import CasLogout from "./auth/CasLogout";
import {authConfig} from "./auth/Auth"; import {authConfig} from "./auth/Auth";
import ProductBuyPage from "./ProductBuyPage";
import PaymentResultPage from "./PaymentResultPage";
class EntryPage extends React.Component { class EntryPage extends React.Component {
constructor(props) { constructor(props) {
@@ -108,7 +110,9 @@ class EntryPage extends React.Component {
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} /> <Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} />
<Route exact path="/select-plan/:owner/:pricingName" render={(props) => this.renderHomeIfLoggedIn(<PricingPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />)} /> <Route exact path="/select-plan/:owner/:pricingName" render={(props) => <PricingPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
<Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
<Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
</Switch> </Switch>
</div> </div>
); );

View File

@@ -101,6 +101,21 @@ class PaymentListPage extends BaseListPage {
); );
}, },
}, },
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{ {
title: i18next.t("general:Provider"), title: i18next.t("general:Provider"),
dataIndex: "provider", dataIndex: "provider",
@@ -117,21 +132,6 @@ class PaymentListPage extends BaseListPage {
); );
}, },
}, },
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{ {
title: i18next.t("general:User"), title: i18next.t("general:User"),
dataIndex: "user", dataIndex: "user",
@@ -158,14 +158,6 @@ class PaymentListPage extends BaseListPage {
return Setting.getFormattedDate(text); return Setting.getFormattedDate(text);
}, },
}, },
// {
// title: i18next.t("general:Display name"),
// dataIndex: 'displayName',
// key: 'displayName',
// width: '160px',
// sorter: true,
// ...this.getColumnSearchProps('displayName'),
// },
{ {
title: i18next.t("provider:Type"), title: i18next.t("provider:Type"),
dataIndex: "type", dataIndex: "type",
@@ -187,6 +179,13 @@ class PaymentListPage extends BaseListPage {
// width: '160px', // width: '160px',
sorter: true, sorter: true,
...this.getColumnSearchProps("productDisplayName"), ...this.getColumnSearchProps("productDisplayName"),
render: (text, record, index) => {
return (
<Link to={`/products/${record.owner}/${record.productName}`}>
{text}
</Link>
);
},
}, },
{ {
title: i18next.t("product:Price"), title: i18next.t("product:Price"),
@@ -265,7 +264,7 @@ class PaymentListPage extends BaseListPage {
value = params.type; value = params.type;
} }
this.setState({loading: true}); this.setState({loading: true});
PaymentBackend.getPayments(Setting.getRequestOrganization(this.props.account), Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) PaymentBackend.getPayments(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => { .then((res) => {
this.setState({ this.setState({
loading: false, loading: false,

View File

@@ -15,17 +15,24 @@
import React from "react"; import React from "react";
import {Button, Result, Spin} from "antd"; import {Button, Result, Spin} from "antd";
import * as PaymentBackend from "./backend/PaymentBackend"; import * as PaymentBackend from "./backend/PaymentBackend";
import * as PricingBackend from "./backend/PricingBackend";
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
class PaymentResultPage extends React.Component { class PaymentResultPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const params = new URLSearchParams(window.location.search);
this.state = { this.state = {
classes: props, classes: props,
paymentName: props.match.params.paymentName, owner: props.match?.params?.organizationName ?? props.match?.params?.owner ?? null,
organizationName: props.match.params.organizationName, paymentName: props.match?.params?.paymentName ?? null,
pricingName: props.pricingName ?? props.match?.params?.pricingName ?? null,
subscriptionName: params.get("subscription"),
payment: null, payment: null,
pricing: props.pricing ?? null,
subscription: props.subscription ?? null,
timeout: null, timeout: null,
}; };
} }
@@ -40,28 +47,77 @@ class PaymentResultPage extends React.Component {
} }
} }
getPayment() { setStateAsync(state) {
PaymentBackend.getPayment(this.state.organizationName, this.state.paymentName) return new Promise((resolve, reject) => {
.then((res) => { this.setState(state, () => {
this.setState({ resolve();
payment: res.data,
});
// window.console.log("payment=", res.data);
if (res.data.state === "Created") {
if (["PayPal", "Stripe"].includes(res.data.type)) {
this.setState({
timeout: setTimeout(() => {
PaymentBackend.notifyPayment(this.state.organizationName, this.state.paymentName)
.then((res) => {
this.getPayment();
});
}, 1000),
});
} else {
this.setState({timeout: setTimeout(() => this.getPayment(), 1000)});
}
}
}); });
});
}
onUpdatePricing(pricing) {
this.props.onUpdatePricing(pricing);
}
async getPayment() {
if (!(this.state.owner && (this.state.paymentName || (this.state.pricingName && this.state.subscriptionName)))) {
return ;
}
try {
// loading price & subscription
if (this.state.pricingName && this.state.subscriptionName) {
if (!this.state.pricing) {
const res = await PricingBackend.getPricing(this.state.owner, this.state.pricingName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const pricing = res.data;
await this.setStateAsync({
pricing: pricing,
});
}
if (!this.state.subscription) {
const res = await SubscriptionBackend.getSubscription(this.state.owner, this.state.subscriptionName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const subscription = res.data;
await this.setStateAsync({
subscription: subscription,
});
}
const paymentName = this.state.subscription.payment;
await this.setStateAsync({
paymentName: paymentName,
});
this.onUpdatePricing(this.state.pricing);
}
const res = await PaymentBackend.getPayment(this.state.owner, this.state.paymentName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const payment = res.data;
await this.setStateAsync({
payment: payment,
});
if (payment.state === "Created") {
if (["PayPal", "Stripe"].includes(payment.type)) {
this.setState({
timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
this.getPayment();
}, 1000),
});
} else {
this.setState({
timeout: setTimeout(() => this.getPayment(), 1000),
});
}
}
} catch (err) {
Setting.showMessage("error", err.message);
return;
}
} }
goToPaymentUrl(payment) { goToPaymentUrl(payment) {
@@ -81,7 +137,7 @@ class PaymentResultPage extends React.Component {
if (payment.state === "Paid") { if (payment.state === "Paid") {
return ( return (
<div> <div className="login-content">
{ {
Setting.renderHelmet(payment) Setting.renderHelmet(payment)
} }
@@ -101,7 +157,7 @@ class PaymentResultPage extends React.Component {
); );
} else if (payment.state === "Created") { } else if (payment.state === "Created") {
return ( return (
<div> <div className="login-content">
{ {
Setting.renderHelmet(payment) Setting.renderHelmet(payment)
} }
@@ -117,7 +173,7 @@ class PaymentResultPage extends React.Component {
); );
} else if (payment.state === "Canceled") { } else if (payment.state === "Canceled") {
return ( return (
<div> <div className="login-content">
{ {
Setting.renderHelmet(payment) Setting.renderHelmet(payment)
} }
@@ -137,7 +193,7 @@ class PaymentResultPage extends React.Component {
); );
} else if (payment.state === "Timeout") { } else if (payment.state === "Timeout") {
return ( return (
<div> <div className="login-content">
{ {
Setting.renderHelmet(payment) Setting.renderHelmet(payment)
} }
@@ -157,7 +213,7 @@ class PaymentResultPage extends React.Component {
); );
} else { } else {
return ( return (
<div> <div className="login-content">
{ {
Setting.renderHelmet(payment) Setting.renderHelmet(payment)
} }

View File

@@ -110,11 +110,12 @@ class PermissionListPage extends BaseListPage {
return ( return (
<Upload {...props}> <Upload {...props}>
<Button type="primary" size="small"> <Button id="upload-button" type="primary" size="small">
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")} <UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
</Button></Upload> </Button></Upload>
); );
} }
renderTable(permissions) { renderTable(permissions) {
const columns = [ const columns = [
// https://github.com/ant-design/ant-design/issues/22184 // https://github.com/ant-design/ant-design/issues/22184
@@ -361,7 +362,7 @@ class PermissionListPage extends BaseListPage {
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addPermission.bind(this)}>{i18next.t("general:Add")}</Button> <Button id="add-button" style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addPermission.bind(this)}>{i18next.t("general:Add")}</Button>
{ {
this.renderPermissionUpload() this.renderPermissionUpload()
} }

View File

@@ -18,6 +18,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as RoleBackend from "./backend/RoleBackend"; import * as RoleBackend from "./backend/RoleBackend";
import * as PlanBackend from "./backend/PlanBackend"; import * as PlanBackend from "./backend/PlanBackend";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
import * as ProviderBackend from "./backend/ProviderBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
@@ -28,14 +29,14 @@ class PlanEditPage extends React.Component {
super(props); super(props);
this.state = { this.state = {
classes: props, classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName, organizationName: props?.organizationName ?? props?.match?.params?.organizationName ?? null,
planName: props.match.params.planName, planName: props?.match?.params?.planName ?? null,
plan: null, plan: null,
organizations: [], organizations: [],
users: [], users: [],
roles: [], roles: [],
providers: [], paymentProviders: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props?.location?.mode ?? "edit",
}; };
} }
@@ -58,6 +59,7 @@ class PlanEditPage extends React.Component {
this.getUsers(this.state.organizationName); this.getUsers(this.state.organizationName);
this.getRoles(this.state.organizationName); this.getRoles(this.state.organizationName);
this.getPaymentProviders(this.state.organizationName);
}); });
} }
@@ -89,6 +91,20 @@ class PlanEditPage extends React.Component {
}); });
} }
getPaymentProviders(organizationName) {
ProviderBackend.getProviders(organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
paymentProviders: res.data.filter(provider => provider.category === "Payment"),
});
return;
}
Setting.showMessage("error", res.msg);
});
}
getOrganizations() { getOrganizations() {
OrganizationBackend.getOrganizations("admin") OrganizationBackend.getOrganizations("admin")
.then((res) => { .then((res) => {
@@ -165,7 +181,7 @@ class PlanEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.role} onChange={(value => {this.updatePlanField("role", value);})} <Select virtual={false} style={{width: "100%"}} value={this.state.plan.role} onChange={(value => {this.updatePlanField("role", value);})}
options={this.state.roles.map((role) => Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`)) options={this.state.roles.map((role) => Setting.getOption(role.name, role.name))
} /> } />
</Col> </Col>
</Row> </Row>
@@ -181,22 +197,27 @@ class PlanEditPage extends React.Component {
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("plan:Price per month"), i18next.t("plan:Price per month - Tooltip"))} : {Setting.getLabel(i18next.t("plan:Price"), i18next.t("plan:Price - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => { <InputNumber value={this.state.plan.price} onChange={value => {
this.updatePlanField("pricePerMonth", value); this.updatePlanField("price", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("plan:Price per year"), i18next.t("plan:Price per year - Tooltip"))} : {Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.plan.pricePerYear} onChange={value => { <Select virtual={false} style={{width: "100%"}} value={this.state.plan.period} onChange={value => {
this.updatePlanField("pricePerYear", value); this.updatePlanField("period", value);
}} /> }}
options={[
{value: "Monthly", label: "Monthly"},
{value: "Yearly", label: "Yearly"},
]}
/>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@@ -216,6 +237,18 @@ class PlanEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.plan.paymentProviders ?? []} onChange={(value => {this.updatePlanField("paymentProviders", value);})}>
{
this.state.paymentProviders.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :

View File

@@ -32,10 +32,11 @@ class PlanListPage extends BaseListPage {
createdTime: moment().format(), createdTime: moment().format(),
displayName: `New Plan - ${randomName}`, displayName: `New Plan - ${randomName}`,
description: "", description: "",
pricePerMonth: 10, price: 10,
pricePerYear: 100,
currency: "USD", currency: "USD",
period: "Monthly",
isEnabled: true, isEnabled: true,
paymentProviders: [],
role: "", role: "",
options: [], options: [],
}; };
@@ -127,18 +128,26 @@ class PlanListPage extends BaseListPage {
...this.getColumnSearchProps("displayName"), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: i18next.t("plan:Price per month"), title: i18next.t("payment:Currency"),
dataIndex: "pricePerMonth", dataIndex: "currency",
key: "pricePerMonth", key: "currency",
width: "130px", width: "120px",
...this.getColumnSearchProps("pricePerMonth"), sorter: true,
...this.getColumnSearchProps("currency"),
}, },
{ {
title: i18next.t("plan:Price per year"), title: i18next.t("plan:Price"),
dataIndex: "pricePerYear", dataIndex: "price",
key: "pricePerYear", key: "price",
width: "130px", width: "130px",
...this.getColumnSearchProps("pricePerYear"), ...this.getColumnSearchProps("price"),
},
{
title: i18next.t("plan:Period"),
dataIndex: "period",
key: "period",
width: "130px",
...this.getColumnSearchProps("period"),
}, },
{ {
title: i18next.t("general:Role"), title: i18next.t("general:Role"),
@@ -148,7 +157,21 @@ class PlanListPage extends BaseListPage {
...this.getColumnSearchProps("role"), ...this.getColumnSearchProps("role"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/roles/${encodeURIComponent(text)}`}> <Link to={`/roles/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("plan:Related product"),
dataIndex: "product",
key: "product",
width: "130px",
...this.getColumnSearchProps("product"),
render: (text, record, index) => {
return (
<Link to={`/products/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
); );

View File

@@ -190,7 +190,7 @@ class PricingEditPage extends React.Component {
onChange={(value => { onChange={(value => {
this.updatePricingField("plans", value); this.updatePricingField("plans", value);
})} })}
options={this.state.plans.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`))} options={this.state.plans.map((plan) => Setting.getOption(plan.name, plan.name))}
/> />
</Col> </Col>
</Row> </Row>
@@ -294,7 +294,7 @@ class PricingEditPage extends React.Component {
</Button> </Button>
</Col> </Col>
<Col> <Col>
<PricingPage pricing={this.state.pricing}></PricingPage> <PricingPage pricing={this.state.pricing} owner={this.state.pricing.owner}></PricingPage>
</Col> </Col>
</React.Fragment> </React.Fragment>
); );

View File

@@ -14,7 +14,8 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Switch, Table} from "antd"; import {Button, Col, Row, Switch, Table, Tooltip} from "antd";
import {EditOutlined} from "@ant-design/icons";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as PricingBackend from "./backend/PricingBackend"; import * as PricingBackend from "./backend/PricingBackend";
@@ -118,11 +119,58 @@ class PricingListPage extends BaseListPage {
title: i18next.t("general:Display name"), title: i18next.t("general:Display name"),
dataIndex: "displayName", dataIndex: "displayName",
key: "displayName", key: "displayName",
// width: "170px", width: "170px",
sorter: true, sorter: true,
...this.getColumnSearchProps("displayName"), ...this.getColumnSearchProps("displayName"),
}, },
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
width: "170px",
sorter: true,
...this.getColumnSearchProps("application"),
render: (text, record, index) => {
return (
<Link to={`/applications/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Plans"),
dataIndex: "plans",
key: "plans",
// width: "170px",
sorter: true,
...this.getColumnSearchProps("plans"),
render: (plans, record, index) => {
if (plans.length === 0) {
return `(${i18next.t("general:empty")})`;
}
return (
<div>
<Row>
{
plans.map((plan) => (
<Col key={plan}>
<div style={{display: "inline", marginRight: "20px"}}>
<Tooltip placement="topLeft" title="Edit">
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/plans/${record.owner}/${plan}`)} />
</Tooltip>
<Link to={`/plans/${record.owner}/${plan}`}>
{plan}
</Link>
</div>
</Col>
))
}
</Row>
</div>
);
},
},
{ {
title: i18next.t("general:Is enabled"), title: i18next.t("general:Is enabled"),
dataIndex: "isEnabled", dataIndex: "isEnabled",

View File

@@ -17,16 +17,24 @@ import {Button, Descriptions, Modal, Spin} from "antd";
import {CheckCircleTwoTone} from "@ant-design/icons"; import {CheckCircleTwoTone} from "@ant-design/icons";
import i18next from "i18next"; import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend"; import * as ProductBackend from "./backend/ProductBackend";
import * as PlanBackend from "./backend/PlanBackend";
import * as PricingBackend from "./backend/PricingBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
class ProductBuyPage extends React.Component { class ProductBuyPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const params = new URLSearchParams(window.location.search);
this.state = { this.state = {
classes: props, classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props?.match?.params?.organizationName, owner: props?.organizationName ?? props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
productName: props.productName !== undefined ? props.productName : props?.match?.params?.productName, productName: props?.productName ?? props?.match?.params?.productName ?? null,
pricingName: props?.pricingName ?? props?.match?.params?.pricingName ?? null,
planName: params.get("plan"),
userName: params.get("user"),
product: null, product: null,
pricing: props?.pricing ?? null,
plan: null,
isPlacingOrder: false, isPlacingOrder: false,
qrCodeModalProvider: null, qrCodeModalProvider: null,
}; };
@@ -36,20 +44,57 @@ class ProductBuyPage extends React.Component {
this.getProduct(); this.getProduct();
} }
getProduct() { setStateAsync(state) {
if (this.state.productName === undefined || this.state.organizationName === undefined) { return new Promise((resolve, reject) => {
this.setState(state, () => {
resolve();
});
});
}
onUpdatePricing(pricing) {
this.props.onUpdatePricing(pricing);
}
async getProduct() {
if (!this.state.owner || (!this.state.productName && !this.state.pricingName)) {
return ; return ;
} }
ProductBackend.getProduct(this.state.organizationName, this.state.productName) try {
.then((res) => { // load pricing & plan
if (res.status === "error") { if (this.state.pricingName) {
Setting.showMessage("error", res.msg); if (!this.state.planName || !this.state.userName) {
return; return ;
} }
this.setState({ let res = await PricingBackend.getPricing(this.state.owner, this.state.pricingName);
product: res.data, if (res.status !== "ok") {
throw new Error(res.msg);
}
const pricing = res.data;
res = await PlanBackend.getPlan(this.state.owner, this.state.planName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const plan = res.data;
await this.setStateAsync({
pricing: pricing,
plan: plan,
productName: plan.product,
}); });
this.onUpdatePricing(pricing);
}
// load product
const res = await ProductBackend.getProduct(this.state.owner, this.state.productName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
this.setState({
product: res.data,
}); });
} catch (err) {
Setting.showMessage("error", err.message);
return;
}
} }
getProductObj() { getProductObj() {
@@ -96,7 +141,7 @@ class ProductBuyPage extends React.Component {
isPlacingOrder: true, isPlacingOrder: true,
}); });
ProductBackend.buyProduct(product.owner, product.name, provider.name) ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "")
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const payUrl = res.data; const payUrl = res.data;
@@ -215,11 +260,11 @@ class ProductBuyPage extends React.Component {
} }
return ( return (
<div> <div className="login-content">
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} > <Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >
<Descriptions title={i18next.t("product:Buy Product")} bordered> <Descriptions title={<span style={{fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
<Descriptions.Item label={i18next.t("general:Name")} span={3}> <Descriptions.Item label={i18next.t("general:Name")} span={3}>
<span style={{fontSize: 28}}> <span style={{fontSize: 25}}>
{Setting.getLanguageText(product?.displayName)} {Setting.getLanguageText(product?.displayName)}
</span> </span>
</Descriptions.Item> </Descriptions.Item>

View File

@@ -98,6 +98,7 @@ class ProductEditPage extends React.Component {
} }
renderProduct() { renderProduct() {
const isCreatedByPlan = this.state.product.tag === "auto_created_product_for_plan";
return ( return (
<Card size="small" title={ <Card size="small" title={
<div> <div>
@@ -107,12 +108,24 @@ class ProductEditPage extends React.Component {
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account) || isCreatedByPlan} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.name} onChange={e => { <Input value={this.state.product.name} disabled={isCreatedByPlan} onChange={e => {
this.updateProductField("name", e.target.value); this.updateProductField("name", e.target.value);
}} /> }} />
</Col> </Col>
@@ -127,18 +140,6 @@ class ProductEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} : {Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
@@ -171,7 +172,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} : {Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.tag} onChange={e => { <Input value={this.state.product.tag} disabled={isCreatedByPlan} onChange={e => {
this.updateProductField("tag", e.target.value); this.updateProductField("tag", e.target.value);
}} /> }} />
</Col> </Col>
@@ -201,7 +202,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} disabled={isCreatedByPlan} onChange={(value => {
this.updateProductField("currency", value); this.updateProductField("currency", value);
})}> })}>
{ {
@@ -218,7 +219,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} : {Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.price} onChange={value => { <InputNumber value={this.state.product.price} disabled={isCreatedByPlan} onChange={value => {
this.updateProductField("price", value); this.updateProductField("price", value);
}} /> }} />
</Col> </Col>
@@ -228,7 +229,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} : {Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.quantity} onChange={value => { <InputNumber value={this.state.product.quantity} disabled={isCreatedByPlan} onChange={value => {
this.updateProductField("quantity", value); this.updateProductField("quantity", value);
}} /> }} />
</Col> </Col>
@@ -238,7 +239,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} : {Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.sold} onChange={value => { <InputNumber value={this.state.product.sold} disabled={isCreatedByPlan} onChange={value => {
this.updateProductField("sold", value); this.updateProductField("sold", value);
}} /> }} />
</Col> </Col>
@@ -248,7 +249,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} : {Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}> <Select virtual={false} mode="multiple" style={{width: "100%"}} disabled={isCreatedByPlan} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}>
{ {
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>) this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
} }
@@ -312,7 +313,7 @@ class ProductEditPage extends React.Component {
submitProductEdit(willExist) { submitProductEdit(willExist) {
const product = Setting.deepCopy(this.state.product); const product = Setting.deepCopy(this.state.product);
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product) ProductBackend.updateProduct(this.state.organizationName, this.state.productName, product)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));

View File

@@ -253,11 +253,13 @@ class ProductListPage extends BaseListPage {
width: "230px", width: "230px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
const isCreatedByPlan = record.tag === "auto_created_product_for_plan";
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
disabled={isCreatedByPlan}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)} onConfirm={() => this.deleteProduct(index)}
> >

View File

@@ -423,13 +423,13 @@ class ProviderEditPage extends React.Component {
[ [
{id: "Captcha", name: "Captcha"}, {id: "Captcha", name: "Captcha"},
{id: "Email", name: "Email"}, {id: "Email", name: "Email"},
{id: "Notification", name: "Notification"},
{id: "OAuth", name: "OAuth"}, {id: "OAuth", name: "OAuth"},
{id: "Payment", name: "Payment"}, {id: "Payment", name: "Payment"},
{id: "SAML", name: "SAML"}, {id: "SAML", name: "SAML"},
{id: "SMS", name: "SMS"}, {id: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"}, {id: "Storage", name: "Storage"},
{id: "Web3", name: "Web3"}, {id: "Web3", name: "Web3"},
{id: "Notification", name: "Notification"},
] ]
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>) .map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)

View File

@@ -142,13 +142,15 @@ class ProviderListPage extends BaseListPage {
key: "category", key: "category",
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: "OAuth", value: "OAuth"}, {text: "Captcha", value: "Captcha"},
{text: "Email", value: "Email"}, {text: "Email", value: "Email"},
{text: "Notification", value: "Notification"},
{text: "OAuth", value: "OAuth"},
{text: "Payment", value: "Payment"},
{text: "SAML", value: "SAML"},
{text: "SMS", value: "SMS"}, {text: "SMS", value: "SMS"},
{text: "Storage", value: "Storage"}, {text: "Storage", value: "Storage"},
{text: "SAML", value: "SAML"}, {text: "Web3", value: "Web3"},
{text: "Captcha", value: "Captcha"},
{text: "Payment", value: "Payment"},
], ],
width: "110px", width: "110px",
sorter: true, sorter: true,
@@ -161,13 +163,15 @@ class ProviderListPage extends BaseListPage {
align: "center", align: "center",
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: "OAuth", value: "OAuth", children: Setting.getProviderTypeOptions("OAuth").map((o) => {return {text: o.id, value: o.name};})}, {text: "Captcha", value: "Captcha", children: Setting.getProviderTypeOptions("Captcha").map((o) => {return {text: o.id, value: o.name};})},
{text: "Email", value: "Email", children: Setting.getProviderTypeOptions("Email").map((o) => {return {text: o.id, value: o.name};})}, {text: "Email", value: "Email", children: Setting.getProviderTypeOptions("Email").map((o) => {return {text: o.id, value: o.name};})},
{text: "Notification", value: "Notification", children: Setting.getProviderTypeOptions("Notification").map((o) => {return {text: o.id, value: o.name};})},
{text: "OAuth", value: "OAuth", children: Setting.getProviderTypeOptions("OAuth").map((o) => {return {text: o.id, value: o.name};})},
{text: "Payment", value: "Payment", children: Setting.getProviderTypeOptions("Payment").map((o) => {return {text: o.id, value: o.name};})},
{text: "SAML", value: "SAML", children: Setting.getProviderTypeOptions("SAML").map((o) => {return {text: o.id, value: o.name};})},
{text: "SMS", value: "SMS", children: Setting.getProviderTypeOptions("SMS").map((o) => {return {text: o.id, value: o.name};})}, {text: "SMS", value: "SMS", children: Setting.getProviderTypeOptions("SMS").map((o) => {return {text: o.id, value: o.name};})},
{text: "Storage", value: "Storage", children: Setting.getProviderTypeOptions("Storage").map((o) => {return {text: o.id, value: o.name};})}, {text: "Storage", value: "Storage", children: Setting.getProviderTypeOptions("Storage").map((o) => {return {text: o.id, value: o.name};})},
{text: "SAML", value: "SAML", children: Setting.getProviderTypeOptions("SAML").map((o) => {return {text: o.id, value: o.name};})}, {text: "Web3", value: "Web3", children: Setting.getProviderTypeOptions("Web3").map((o) => {return {text: o.id, value: o.name};})},
{text: "Captcha", value: "Captcha", children: Setting.getProviderTypeOptions("Captcha").map((o) => {return {text: o.id, value: o.name};})},
{text: "Payment", value: "Payment", children: Setting.getProviderTypeOptions("Payment").map((o) => {return {text: o.id, value: o.name};})},
], ],
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
@@ -237,7 +241,7 @@ class ProviderListPage extends BaseListPage {
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Providers")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Providers")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button> <Button id="add-button" type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button>
</div> </div>
)} )}
loading={this.state.loading} loading={this.state.loading}

View File

@@ -76,7 +76,7 @@ class ResourceListPage extends BaseListPage {
return ( return (
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false} <Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false}
beforeUpload={file => {return false;}} onChange={info => {this.handleUpload(info);}}> beforeUpload={file => {return false;}} onChange={info => {this.handleUpload(info);}}>
<Button icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small"> <Button id="upload-button" icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
{i18next.t("resource:Upload a file...")} {i18next.t("resource:Upload a file...")}
</Button> </Button>
</Upload> </Upload>

View File

@@ -274,7 +274,7 @@ export const OtherProviderInfo = {
}, },
"Custom HTTP": { "Custom HTTP": {
logo: `${StaticBaseUrl}/img/email_default.png`, logo: `${StaticBaseUrl}/img/email_default.png`,
url: "https://casdoor.org/docs/provider/sms/overview", url: "https://casdoor.org/docs/provider/notification/overview",
}, },
}, },
}; };
@@ -927,7 +927,7 @@ export function getProviderTypeOptions(category) {
{id: "Local File System", name: "Local File System"}, {id: "Local File System", name: "Local File System"},
{id: "AWS S3", name: "AWS S3"}, {id: "AWS S3", name: "AWS S3"},
{id: "MinIO", name: "MinIO"}, {id: "MinIO", name: "MinIO"},
{id: "Aliyun OSS", name: "Aliyun OSS"}, {id: "Aliyun OSS", name: "Alibaba Cloud OSS"},
{id: "Tencent Cloud COS", name: "Tencent Cloud COS"}, {id: "Tencent Cloud COS", name: "Tencent Cloud COS"},
{id: "Azure Blob", name: "Azure Blob"}, {id: "Azure Blob", name: "Azure Blob"},
{id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"}, {id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"},
@@ -1170,9 +1170,9 @@ export function getTags(tags, urlPrefix = null) {
return res; return res;
} }
export function getTag(color, text) { export function getTag(color, text, icon) {
return ( return (
<Tag color={color}> <Tag color={color} icon={icon}>
{text} {text}
</Tag> </Tag>
); );
@@ -1254,3 +1254,13 @@ export function builtInObject(obj) {
} }
return obj.owner === "built-in" && BuiltInObjects.includes(obj.name); return obj.owner === "built-in" && BuiltInObjects.includes(obj.name);
} }
export function getCurrencySymbol(currency) {
if (currency === "USD" || currency === "usd") {
return "$";
} else if (currency === "CNY" || currency === "cny") {
return "¥";
} else {
return currency;
}
}

View File

@@ -14,8 +14,9 @@
import moment from "moment"; import moment from "moment";
import React from "react"; import React from "react";
import {Button, Card, Col, DatePicker, Input, InputNumber, Row, Select, Switch} from "antd"; import {Button, Card, Col, DatePicker, Input, Row, Select} from "antd";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as PricingBackend from "./backend/PricingBackend";
import * as PlanBackend from "./backend/PlanBackend"; import * as PlanBackend from "./backend/PlanBackend";
import * as SubscriptionBackend from "./backend/SubscriptionBackend"; import * as SubscriptionBackend from "./backend/SubscriptionBackend";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
@@ -33,7 +34,8 @@ class SubscriptionEditPage extends React.Component {
subscription: null, subscription: null,
organizations: [], organizations: [],
users: [], users: [],
planes: [], pricings: [],
plans: [],
providers: [], providers: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
@@ -62,15 +64,25 @@ class SubscriptionEditPage extends React.Component {
}); });
this.getUsers(this.state.organizationName); this.getUsers(this.state.organizationName);
this.getPlanes(this.state.organizationName); this.getPricings(this.state.organizationName);
this.getPlans(this.state.organizationName);
}); });
} }
getPlanes(organizationName) { getPricings(organizationName) {
PricingBackend.getPricings(organizationName)
.then((res) => {
this.setState({
pricings: res.data,
});
});
}
getPlans(organizationName) {
PlanBackend.getPlans(organizationName) PlanBackend.getPlans(organizationName)
.then((res) => { .then((res) => {
this.setState({ this.setState({
planes: res.data, plans: res.data,
}); });
}); });
} }
@@ -133,7 +145,7 @@ class SubscriptionEditPage extends React.Component {
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.owner} onChange={(owner => { <Select virtual={false} style={{width: "100%"}} value={this.state.subscription.owner} onChange={(owner => {
this.updateSubscriptionField("owner", owner); this.updateSubscriptionField("owner", owner);
this.getUsers(owner); this.getUsers(owner);
this.getPlanes(owner); this.getPlans(owner);
})} })}
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name)) options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
} /> } />
@@ -161,32 +173,39 @@ class SubscriptionEditPage extends React.Component {
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("subscription:Duration"), i18next.t("subscription:Duration - Tooltip"))} {Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.subscription.duration} onChange={value => { <DatePicker value={dayjs(this.state.subscription.startTime)} onChange={value => {
this.updateSubscriptionField("duration", value); this.updateSubscriptionField("startTime", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("subscription:Start date"), i18next.t("subscription:Start date - Tooltip"))} {Setting.getLabel(i18next.t("subscription:End time"), i18next.t("subscription:End time - Tooltip"))}
</Col> </Col>
<Col span={22} > <Col span={22} >
<DatePicker value={dayjs(this.state.subscription.startDate)} onChange={value => { <DatePicker value={dayjs(this.state.subscription.endTime)} onChange={value => {
this.updateSubscriptionField("startDate", value); this.updateSubscriptionField("endTime", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("subscription:End date"), i18next.t("subscription:End date - Tooltip"))} {Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<DatePicker value={dayjs(this.state.subscription.endDate)} onChange={value => { <Select
this.updateSubscriptionField("endDate", value); defaultValue={this.state.subscription.period === "" ? "Monthly" : this.state.subscription.period}
}} /> onChange={value => {
this.updateSubscriptionField("period", value);
}}
options={[
{value: "Monthly", label: "Monthly"},
{value: "Yearly", label: "Yearly"},
]}
/>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@@ -196,21 +215,42 @@ class SubscriptionEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select style={{width: "100%"}} value={this.state.subscription.user} <Select style={{width: "100%"}} value={this.state.subscription.user}
onChange={(value => {this.updateSubscriptionField("user", value);})} onChange={(value => {this.updateSubscriptionField("user", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))} options={this.state.users.map((user) => Setting.getOption(user.name, user.name))}
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Pricing"), i18next.t("general:Pricing - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.pricing}
onChange={(value => {this.updateSubscriptionField("pricing", value);})}
options={this.state.pricings.map((pricing) => Setting.getOption(pricing.name, pricing.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Plan"), i18next.t("general:Plan - Tooltip"))} : {Setting.getLabel(i18next.t("general:Plan"), i18next.t("general:Plan - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.plan} onChange={(value => {this.updateSubscriptionField("plan", value);})} <Select virtual={false} style={{width: "100%"}} value={this.state.subscription.plan}
options={this.state.planes.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`)) onChange={(value => {this.updateSubscriptionField("plan", value);})}
options={this.state.plans.map((plan) => Setting.getOption(plan.name, plan.name))
} /> } />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Payment"), i18next.t("general:Payment - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.subscription.payment} disabled={true} onChange={e => {
this.updateSubscriptionField("payment", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} : {Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
@@ -221,46 +261,6 @@ class SubscriptionEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.subscription.isEnabled} onChange={checked => {
this.updateSubscriptionField("isEnabled", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Submitter"), i18next.t("permission:Submitter - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.subscription.submitter} onChange={e => {
this.updateSubscriptionField("submitter", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Approver"), i18next.t("permission:Approver - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.subscription.approver} onChange={e => {
this.updateSubscriptionField("approver", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Approve time"), i18next.t("permission:Approve time - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={Setting.getFormattedDate(this.state.subscription.approveTime)} onChange={e => {
this.updatePermissionField("approveTime", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} : {Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
@@ -280,8 +280,12 @@ class SubscriptionEditPage extends React.Component {
this.updateSubscriptionField("state", value); this.updateSubscriptionField("state", value);
})} })}
options={[ options={[
{value: "Approved", name: i18next.t("permission:Approved")},
{value: "Pending", name: i18next.t("permission:Pending")}, {value: "Pending", name: i18next.t("permission:Pending")},
{value: "Active", name: i18next.t("permission:Active")},
{value: "Upcoming", name: i18next.t("permission:Upcoming")},
{value: "Expired", name: i18next.t("permission:Expired")},
{value: "Error", name: i18next.t("permission:Error")},
{value: "Suspended", name: i18next.t("permission:Suspended")},
].map((item) => Setting.getOption(item.name, item.value))} ].map((item) => Setting.getOption(item.name, item.value))}
/> />
</Col> </Col>

View File

@@ -15,6 +15,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Table} from "antd"; import {Button, Table} from "antd";
import {ClockCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, MinusCircleOutlined, SyncOutlined} from "@ant-design/icons";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as SubscriptionBackend from "./backend/SubscriptionBackend"; import * as SubscriptionBackend from "./backend/SubscriptionBackend";
@@ -26,24 +27,19 @@ class SubscriptionListPage extends BaseListPage {
newSubscription() { newSubscription() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
const owner = Setting.getRequestOrganization(this.props.account); const owner = Setting.getRequestOrganization(this.props.account);
const defaultDuration = 365;
return { return {
owner: owner, owner: owner,
name: `subscription_${randomName}`, name: `sub_${randomName}`,
createdTime: moment().format(), createdTime: moment().format(),
displayName: `New Subscription - ${randomName}`, displayName: `New Subscription - ${randomName}`,
startDate: moment().format(), startTime: moment().format(),
endDate: moment().add(defaultDuration, "d").format(), endTime: moment().add(30, "d").format(),
duration: defaultDuration, period: "Monthly",
description: "", description: "",
user: "", user: "",
plan: "", plan: "",
isEnabled: true, state: "Active",
submitter: this.props.account.name,
approver: this.props.account.name,
approveTime: moment().format(),
state: "Approved",
}; };
} }
@@ -133,11 +129,25 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("displayName"), ...this.getColumnSearchProps("displayName"),
}, },
{ {
title: i18next.t("subscription:Duration"), title: i18next.t("subscription:Period"),
dataIndex: "duration", dataIndex: "period",
key: "duration", key: "period",
width: "140px", width: "140px",
...this.getColumnSearchProps("duration"), ...this.getColumnSearchProps("period"),
},
{
title: i18next.t("subscription:Start time"),
dataIndex: "startTime",
key: "startTime",
width: "140px",
...this.getColumnSearchProps("startTime"),
},
{
title: i18next.t("subscription:End time"),
dataIndex: "endTime",
key: "endTime",
width: "140px",
...this.getColumnSearchProps("endTime"),
}, },
{ {
title: i18next.t("general:Plan"), title: i18next.t("general:Plan"),
@@ -147,7 +157,7 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("plan"), ...this.getColumnSearchProps("plan"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/plans/${text}`}> <Link to={`/plans/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
); );
@@ -161,7 +171,21 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("user"), ...this.getColumnSearchProps("user"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/users/${text}`}> <Link to={`/users/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Payment"),
dataIndex: "payment",
key: "payment",
width: "140px",
...this.getColumnSearchProps("payment"),
render: (text, record, index) => {
return (
<Link to={`/payments/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
); );
@@ -176,10 +200,18 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("state"), ...this.getColumnSearchProps("state"),
render: (text, record, index) => { render: (text, record, index) => {
switch (text) { switch (text) {
case "Approved":
return Setting.getTag("success", i18next.t("permission:Approved"));
case "Pending": case "Pending":
return Setting.getTag("error", i18next.t("permission:Pending")); return Setting.getTag("processing", i18next.t("permission:Pending"), <ExclamationCircleOutlined />);
case "Active":
return Setting.getTag("success", i18next.t("permission:Active"), <SyncOutlined spin />);
case "Upcoming":
return Setting.getTag("warning", i18next.t("permission:Upcoming"), <ClockCircleOutlined />);
case "Expired":
return Setting.getTag("warning", i18next.t("permission:Expired"), <ClockCircleOutlined />);
case "Error":
return Setting.getTag("error", i18next.t("permission:Error"), <CloseCircleOutlined />);
case "Suspended":
return Setting.getTag("default", i18next.t("permission:Suspended"), <MinusCircleOutlined />);
default: default:
return null; return null;
} }

View File

@@ -234,12 +234,60 @@ class SyncerEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.databaseType} onChange={(value => {
this.updateSyncerField("databaseType", value);
if (value === "postgres") {
this.updateSyncerField("sslMode", "disable");
} else {
this.updateSyncerField("sslMode", "");
}
})}>
{
[
{id: "mysql", name: "MySQL"},
{id: "postgres", name: "PostgreSQL"},
{id: "mssql", name: "SQL Server"},
{id: "oracle", name: "Oracle"},
{id: "sqlite3", name: "Sqlite 3"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.syncer.databaseType !== "postgres" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:SSL mode"), i18next.t("syncer:SSL mode - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.sslMode} onChange={(value => {this.updateSyncerField("sslMode", value);})}>
{
[
{id: "disable", name: "disable"},
// {id: "allow", name: "allow"},
// {id: "prefer", name: "prefer"},
{id: "require", name: "require"},
{id: "verify-ca", name: "verify-ca"},
{id: "verify-full", name: "verify-full"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.syncer.host} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.syncer.host} onChange={e => {
this.updateSyncerField("host", e.target.value); this.updateSyncerField("host", e.target.value);
}} /> }} />
</Col> </Col>
@@ -274,24 +322,6 @@ class SyncerEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.databaseType} onChange={(value => {this.updateSyncerField("databaseType", value);})}>
{
[
{id: "mysql", name: "MySQL"},
{id: "postgres", name: "PostgreSQL"},
{id: "mssql", name: "SQL Server"},
{id: "oracle", name: "Oracle"},
{id: "sqlite3", name: "Sqlite 3"},
].map((databaseType, index) => <Option key={index} value={databaseType.id}>{databaseType.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} : {Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :

View File

@@ -86,8 +86,13 @@ class SyncerListPage extends BaseListPage {
this.setState({loading: true}); this.setState({loading: true});
SyncerBackend.runSyncer("admin", this.state.data[i].name) SyncerBackend.runSyncer("admin", this.state.data[i].name)
.then((res) => { .then((res) => {
this.setState({loading: false}); if (res.status === "ok") {
Setting.showMessage("success", "Syncer sync users successfully"); this.setState({loading: false});
Setting.showMessage("success", i18next.t("general:Successfully synced"));
} else {
this.setState({loading: false});
Setting.showMessage("error", `${i18next.t("general:Failed to sync")}: ${res.msg}`);
}
} }
) )
.catch(error => { .catch(error => {
@@ -151,6 +156,13 @@ class SyncerListPage extends BaseListPage {
{text: "LDAP", value: "LDAP"}, {text: "LDAP", value: "LDAP"},
], ],
}, },
{
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "130px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{ {
title: i18next.t("provider:Host"), title: i18next.t("provider:Host"),
dataIndex: "host", dataIndex: "host",
@@ -183,13 +195,6 @@ class SyncerListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("password"), ...this.getColumnSearchProps("password"),
}, },
{
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "120px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{ {
title: i18next.t("syncer:Database"), title: i18next.t("syncer:Database"),
dataIndex: "database", dataIndex: "database",
@@ -208,7 +213,7 @@ class SyncerListPage extends BaseListPage {
title: i18next.t("syncer:Sync interval"), title: i18next.t("syncer:Sync interval"),
dataIndex: "syncInterval", dataIndex: "syncInterval",
key: "syncInterval", key: "syncInterval",
width: "130px", width: "140px",
sorter: true, sorter: true,
...this.getColumnSearchProps("syncInterval"), ...this.getColumnSearchProps("syncInterval"),
}, },

View File

@@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {Card, Col, Divider, Progress, Row, Spin} from "antd"; import {Card, Col, Divider, Progress, Row, Spin, Tour} from "antd";
import * as SystemBackend from "./backend/SystemInfo"; import * as SystemBackend from "./backend/SystemInfo";
import React from "react"; import React from "react";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as TourConfig from "./TourConfig";
import i18next from "i18next"; import i18next from "i18next";
import PrometheusInfoTable from "./table/PrometheusInfoTable"; import PrometheusInfoTable from "./table/PrometheusInfoTable";
@@ -29,6 +30,7 @@ class SystemInfo extends React.Component {
prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0}, prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
intervalId: null, intervalId: null,
loading: true, loading: true,
isTourVisible: TourConfig.getTourVisible(),
}; };
} }
@@ -67,12 +69,48 @@ class SystemInfo extends React.Component {
}); });
} }
componentDidMount() {
window.addEventListener("storageTourChanged", this.handleTourChange);
}
handleTourChange = () => {
this.setState({isTourVisible: TourConfig.getTourVisible()});
};
componentWillUnmount() { componentWillUnmount() {
if (this.state.intervalId !== null) { if (this.state.intervalId !== null) {
clearInterval(this.state.intervalId); clearInterval(this.state.intervalId);
} }
window.removeEventListener("storageTourChanged", this.handleTourChange);
} }
setIsTourVisible = () => {
TourConfig.setIsTourVisible(false);
this.setState({isTourVisible: false});
};
handleTourComplete = () => {
const nextPathName = TourConfig.getNextUrl();
if (nextPathName !== "") {
this.props.history.push("/" + nextPathName);
TourConfig.setIsTourVisible(true);
}
};
getSteps = () => {
const nextPathName = TourConfig.getNextUrl();
const steps = TourConfig.getSteps();
steps.map((item, index) => {
item.target = () => document.getElementById(item.id) || null;
if (index === steps.length - 1) {
item.nextButtonProps = {
children: TourConfig.getNextButtonChild(nextPathName),
};
}
});
return steps;
};
render() { render() {
const cpuUi = this.state.systemInfo.cpuUsage?.length <= 0 ? i18next.t("system:Failed to get CPU usage") : const cpuUi = this.state.systemInfo.cpuUsage?.length <= 0 ? i18next.t("system:Failed to get CPU usage") :
this.state.systemInfo.cpuUsage.map((usage, i) => { this.state.systemInfo.cpuUsage.map((usage, i) => {
@@ -99,45 +137,58 @@ class SystemInfo extends React.Component {
if (!Setting.isMobile()) { if (!Setting.isMobile()) {
return ( return (
<Row> <>
<Col span={6}></Col> <Row>
<Col span={12}> <Col span={6}></Col>
<Row gutter={[10, 10]}> <Col span={12}>
<Col span={12}> <Row gutter={[10, 10]}>
<Card title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}> <Col span={12}>
{this.state.loading ? <Spin size="large" /> : cpuUi} <Card id="cpu-card" title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
</Card> {this.state.loading ? <Spin size="large" /> : cpuUi}
</Col> </Card>
<Col span={12}> </Col>
<Card title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}> <Col span={12}>
{this.state.loading ? <Spin size="large" /> : memUi} <Card id="memory-card" title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
</Card> {this.state.loading ? <Spin size="large" /> : memUi}
</Col> </Card>
<Col span={24}> </Col>
<Card title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}> <Col span={24}>
{this.state.loading ? <Spin size="large" /> : latencyUi} <Card id="latency-card" title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}>
</Card> {this.state.loading ? <Spin size="large" /> : latencyUi}
</Col> </Card>
<Col span={24}> </Col>
<Card title={i18next.t("system:API Throughput")} bordered={true} style={{textAlign: "center", height: "100%"}}> <Col span={24}>
{this.state.loading ? <Spin size="large" /> : throughputUi} <Card id="throughput-card" title={i18next.t("system:API Throughput")} bordered={true} style={{textAlign: "center", height: "100%"}}>
</Card> {this.state.loading ? <Spin size="large" /> : throughputUi}
</Col> </Card>
</Row> </Col>
<Divider /> </Row>
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}> <Divider />
<div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div> <Card id="about-card" title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
GitHub: <a target="_blank" rel="noreferrer" href="https://github.com/casdoor/casdoor">Casdoor</a> <div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div>
<br /> GitHub: <a target="_blank" rel="noreferrer" href="https://github.com/casdoor/casdoor">Casdoor</a>
{i18next.t("system:Version")}: <a target="_blank" rel="noreferrer" href={link}>{versionText}</a> <br />
<br /> {i18next.t("system:Version")}: <a target="_blank" rel="noreferrer" href={link}>{versionText}</a>
{i18next.t("system:Official website")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org">https://casdoor.org</a> <br />
<br /> {i18next.t("system:Official website")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org">https://casdoor.org</a>
{i18next.t("system:Community")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org/#:~:text=Casdoor%20API-,Community,-GitHub">Get in Touch!</a> <br />
</Card> {i18next.t("system:Community")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org/#:~:text=Casdoor%20API-,Community,-GitHub">Get in Touch!</a>
</Col> </Card>
<Col span={6}></Col> </Col>
</Row> <Col span={6}></Col>
</Row>
<Tour
open={this.state.isTourVisible}
onClose={this.setIsTourVisible}
steps={this.getSteps()}
indicatorsRender={(current, total) => (
<span>
{current + 1} / {total}
</span>
)}
onFinish={this.handleTourComplete}
/>
</>
); );
} else { } else {
return ( return (

229
web/src/TourConfig.js Normal file
View File

@@ -0,0 +1,229 @@
import React from "react";
export const TourObj = {
home: [
{
title: "Welcome to casdoor",
description: "You can learn more about the use of CasDoor at https://casdoor.org/.",
cover: (
<img
alt="casdoor.png"
src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png"
/>
),
},
{
title: "Statistic cards",
description: "Here are four statistic cards for user information.",
id: "statistic",
},
{
title: "Import users",
description: "You can add new users or update existing Casdoor users by uploading a XLSX file of user information.",
id: "echarts-chart",
},
],
webhooks: [
{
title: "Webhook List",
description: "Event systems allow you to build integrations, which subscribe to certain events on Casdoor. When one of those event is triggered, we'll send a POST json payload to the configured URL. The application parsed the json payload and carry out the hooked function. Events consist of signup, login, logout, update users, which are stored in the action field of the record. Event systems can be used to update an external issue from users.",
},
],
syncers: [
{
title: "Syncer List",
description: "Casdoor stores users in user table. Don't worry about migrating your application user data into Casdoor, when you plan to use Casdoor as an authentication platform. Casdoor provides syncer to quickly help you sync user data to Casdoor.",
},
],
sysinfo: [
{
title: "CPU Usage",
description: "You can see the CPU usage in real time.",
id: "cpu-card",
},
{
title: "Memory Usage",
description: "You can see the Memory usage in real time.",
id: "memory-card",
},
{
title: "API Latency",
description: "You can see the usage statistics of each API latency in real time.",
id: "latency-card",
},
{
title: "API Throughput",
description: "You can see the usage statistics of each API throughput in real time.",
id: "throughput-card",
},
{
title: "About Casdoor",
description: "You can get more Casdoor information in this card.",
id: "about-card",
},
],
subscriptions: [
{
title: "Subscription List",
description: "Subscription helps to manage user's selected plan that make easy to control application's features access.",
},
],
pricings: [
{
title: "Price List",
description: "Casdoor can be used as subscription management system via plan, pricing and subscription.",
},
],
plans: [
{
title: "Plan List",
description: "Plan describe list of application's features with own name and price. Plan features depends on Casdoor role with set of permissions.That allow to describe plan's features independ on naming and price. For example: plan may has diffrent prices depends on county or date.",
},
],
payments: [
{
title: "Payment List",
description: "After the payment is successful, you can see the transaction information of the products in Payment, such as organization, user, purchase time, product name, etc.",
},
],
products: [
{
title: "Session List",
description: "You can add the product (or service) you want to sell. The following will tell you how to add a product.",
},
],
sessions: [
{
title: "Session List",
description: "You can get Session ID in this list.",
},
],
tokens: [
{
title: "Token List",
description: "Casdoor is based on OAuth. Tokens are users' OAuth token.You can get access token in this list.",
},
],
enforcers: [
{
title: "Enforcer List",
description: "In addition to the API interface for requesting enforcement of permission control, Casdoor also provides other interfaces that help external applications obtain permission policy information, which is also listed here.",
},
],
adapters: [
{
title: "Adapter List",
description: "Casdoor supports using the UI to connect the adapter and manage the policy rules. In Casbin, the policy storage is implemented as an adapter (aka middleware for Casbin). A Casbin user can use an adapter to load policy rules from a storage, or save policy rules to it.",
},
],
models: [
{
title: "Model List",
description: "Model defines your permission policy structure, and how requests should match these permission policies and their effects. Then you can user model in Permission.",
},
],
permissions: [
{
title: "Permission List",
description: "All users associated with a single Casdoor organization are shared between the organization's applications and therefore have access to the applications. Sometimes you may want to restrict users' access to certain applications, or certain resources in a certain application. In this case, you can use Permission implemented by Casbin.",
},
{
title: "Permission Add",
description: "In the Casdoor Web UI, you can add a Model for your organization in the Model configuration item, and a Policy for your organization in the Permission configuration item. ",
id: "add-button",
},
{
title: "Permission Upload",
description: "With Casbin Online Editor, you can get Model and Policy files suitable for your usage scenarios. You can easily import the Model file into Casdoor through the Casdoor Web UI for use by the built-in Casbin. ",
id: "upload-button",
},
],
roles: [
{
title: "Role List",
description: "Each user may have multiple roles. You can see the user's roles on the user's profile.",
},
],
resources: [
{
title: "Resource List",
description: "You can upload resources in casdoor. Before upload resources, you need to configure a storage provider. Please see Storage Provider.",
},
{
title: "Upload Resource",
description: "Users can upload resources such as files and images to the previously configured cloud storage.",
id: "upload-button",
},
],
providers: [
{
title: "Provider List",
description: "We have 6 kinds of providers:OAuth providers、SMS Providers、Email Providers、Storage Providers、Payment Provider、Captcha Provider.",
},
{
title: "Provider Add",
description: "You must add the provider to application, then you can use the provider in your application",
id: "add-button",
},
],
organizations: [
{
title: "Organization List",
description: "Organization is the basic unit of Casdoor, which manages users and applications. If a user signed in to an organization, then he can access all applications belonging to the organization without signing in again.",
},
],
groups: [
{
title: "Group List",
description: "In the groups list pages, you can see all the groups in organizations.",
},
],
users: [
{
title: "User List",
description: "As an authentication platform, Casdoor is able to manage users.",
},
{
title: "Import users",
description: "You can add new users or update existing Casdoor users by uploading a XLSX file of user information.",
id: "upload-button",
},
],
applications: [
{
title: "Application List",
description: "If you want to use Casdoor to provide login service for your web Web APPs, you can add them as Casdoor applications. Users can access all applications in their organizations without login twice.",
},
],
};
export const TourUrlList = ["home", "organizations", "groups", "users", "applications", "providers", "resources", "roles", "permissions", "models", "adapters", "enforcers", "tokens", "sessions", "products", "payments", "plans", "pricings", "subscriptions", "sysinfo", "syncers", "webhooks"];
export function getNextUrl(pathName = window.location.pathname) {
return TourUrlList[TourUrlList.indexOf(pathName.replace("/", "")) + 1] || "";
}
export function setIsTourVisible(visible) {
localStorage.setItem("isTourVisible", visible);
window.dispatchEvent(new Event("storageTourChanged"));
}
export function getTourVisible() {
return localStorage.getItem("isTourVisible") !== "false";
}
export function getNextButtonChild(nextPathName) {
return nextPathName !== "" ?
`Go to "${nextPathName.charAt(0).toUpperCase()}${nextPathName.slice(1)} List"`
: "Finish";
}
export function getSteps() {
const path = window.location.pathname.replace("/", "");
const res = TourObj[path];
if (res === undefined) {
return [];
} else {
return res;
}
}

View File

@@ -390,7 +390,7 @@ class UserEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.user.type} onChange={(value => {this.updateUserField("type", value);})} <Select virtual={false} style={{width: "100%"}} value={this.state.user.type} onChange={(value => {this.updateUserField("type", value);})}
options={["normal-user"].map(item => Setting.getOption(item, item))} options={["normal-user", "paid-user"].map(item => Setting.getOption(item, item))}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -187,7 +187,7 @@ class UserListPage extends BaseListPage {
return ( return (
<Upload {...props}> <Upload {...props}>
<Button type="primary" size="small"> <Button id="upload-button" type="primary" size="small">
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")} <UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
</Button> </Button>
</Upload> </Upload>
@@ -426,7 +426,7 @@ class UserListPage extends BaseListPage {
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Users")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Users")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addUser.bind(this)}>{i18next.t("general:Add")}</Button> <Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addUser.bind(this)}>{i18next.t("general:Add")} </Button>
{ {
this.renderUpload() this.renderUpload()
} }

View File

@@ -159,6 +159,17 @@ class WebhookEditPage extends React.Component {
}); });
} }
getApiPaths() {
const objects = ["organization", "group", "user", "application", "provider", "resource", "cert", "role", "permission", "model", "adapter", "enforcer", "session", "record", "token", "product", "payment", "plan", "pricing", "subscription", "syncer", "webhook"];
const res = [];
objects.forEach(obj => {
["add", "update", "delete"].forEach(action => {
res.push(`${action}-${obj}`);
});
});
return res;
}
renderWebhook() { renderWebhook() {
const preview = Setting.deepCopy(previewTemplate); const preview = Setting.deepCopy(previewTemplate);
if (this.state.webhook.isUserExtended) { if (this.state.webhook.isUserExtended) {
@@ -263,7 +274,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", "update-subscription"].map((option, index) => { ["signup", "login", "logout"].concat(this.getApiPaths()).map((option, index) => {
return ( return (
<Option key={option} value={option}>{option}</Option> <Option key={option} value={option}>{option}</Option>
); );

View File

@@ -403,6 +403,14 @@ class LoginPage extends React.Component {
/>); />);
}, },
}); });
} else if (res.data === "SelectPlan") {
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
const pricing = res.data2;
Setting.goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${values.username}`);
} else if (res.data === "BuyPlanResult") {
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
const sub = res.data2;
Setting.goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
} else { } else {
callback(res); callback(res);
} }

View File

@@ -133,7 +133,11 @@ class SignupPage extends React.Component {
}); });
} }
getResultPath(application) { getResultPath(application, signupParams) {
if (signupParams?.plan && signupParams?.pricing) {
// the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page
return `/buy-plan/${application.organization}/${signupParams?.pricing}?user=${signupParams.username}&plan=${signupParams.plan}`;
}
if (authConfig.appName === application.name) { if (authConfig.appName === application.name) {
return "/result"; return "/result";
} else { } else {
@@ -173,13 +177,12 @@ class SignupPage extends React.Component {
const application = this.getApplicationObj(); const application = this.getApplicationObj();
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
values["plan"] = params.get("plan"); values.plan = params.get("plan");
values["pricing"] = params.get("pricing"); values.pricing = params.get("pricing");
AuthBackend.signup(values) AuthBackend.signup(values)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
if (Setting.hasPromptPage(application)) { if (Setting.hasPromptPage(application) && (!values.plan || !values.pricing)) {
AuthBackend.getAccount("") AuthBackend.getAccount("")
.then((res) => { .then((res) => {
let account = null; let account = null;
@@ -188,13 +191,13 @@ class SignupPage extends React.Component {
account.organization = res.data2; account.organization = res.data2;
this.onUpdateAccount(account); this.onUpdateAccount(account);
Setting.goToLinkSoft(this, this.getResultPath(application)); Setting.goToLinkSoft(this, this.getResultPath(application, values));
} else { } else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
} }
}); });
} else { } else {
Setting.goToLinkSoft(this, this.getResultPath(application)); Setting.goToLinkSoft(this, this.getResultPath(application, values));
} }
} else { } else {
Setting.showMessage("error", i18next.t(`signup:${res.msg}`)); Setting.showMessage("error", i18next.t(`signup:${res.msg}`));

View File

@@ -14,8 +14,8 @@
import * as Setting from "../Setting"; import * as Setting from "../Setting";
export function getPayments(owner, organization, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") { export function getPayments(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&organization=${organization}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, { return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -24,18 +24,8 @@ export function getPlans(owner, page = "", pageSize = "", field = "", value = ""
}).then(res => res.json()); }).then(res => res.json());
} }
export function getPlanById(id, includeOption = false) { export function getPlan(owner, name, includeOption = false) {
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${id}&includeOption=${includeOption}`, { return fetch(`${Setting.ServerUrl}/api/get-plan?id=${owner}/${encodeURIComponent(name)}&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", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

View File

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

View File

@@ -37,7 +37,7 @@ const AppListPage = (props) => {
return applications.map(application => { return applications.map(application => {
let homepageUrl = application.homepageUrl; let homepageUrl = application.homepageUrl;
if (homepageUrl === "<custom-url>") { if (homepageUrl === "<custom-url>") {
homepageUrl = this.props.account.homepage; homepageUrl = props.account.homepage;
} }
return { return {

View File

@@ -13,15 +13,23 @@
// limitations under the License. // limitations under the License.
import {ArrowUpOutlined} from "@ant-design/icons"; import {ArrowUpOutlined} from "@ant-design/icons";
import {Card, Col, Row, Statistic} from "antd"; import {Card, Col, Row, Statistic, Tour} from "antd";
import * as echarts from "echarts"; import * as echarts from "echarts";
import i18next from "i18next"; import i18next from "i18next";
import React from "react"; import React from "react";
import * as DashboardBackend from "../backend/DashboardBackend"; import * as DashboardBackend from "../backend/DashboardBackend";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import * as TourConfig from "../TourConfig";
const Dashboard = (props) => { const Dashboard = (props) => {
const [dashboardData, setDashboardData] = React.useState(null); const [dashboardData, setDashboardData] = React.useState(null);
const [isTourVisible, setIsTourVisible] = React.useState(TourConfig.getTourVisible());
const nextPathName = TourConfig.getNextUrl("home");
React.useEffect(() => {
window.addEventListener("storageTourChanged", handleTourChange);
return () => window.removeEventListener("storageTourChanged", handleTourChange);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (!Setting.isLocalAdminUser(props.account)) { if (!Setting.isLocalAdminUser(props.account)) {
@@ -42,6 +50,35 @@ const Dashboard = (props) => {
}); });
}, [props.owner]); }, [props.owner]);
const handleTourChange = () => {
setIsTourVisible(TourConfig.getTourVisible());
};
const setIsTourToLocal = () => {
TourConfig.setIsTourVisible(false);
setIsTourVisible(false);
};
const handleTourComplete = () => {
if (nextPathName !== "") {
props.history.push("/" + nextPathName);
TourConfig.setIsTourVisible(true);
}
};
const getSteps = () => {
const steps = TourConfig.TourObj["home"];
steps.map((item, index) => {
item.target = () => document.getElementById(item.id) || null;
if (index === steps.length - 1) {
item.nextButtonProps = {
children: TourConfig.getNextButtonChild(nextPathName),
};
}
});
return steps;
};
const renderEChart = () => { const renderEChart = () => {
if (dashboardData === null) { if (dashboardData === null) {
return; return;
@@ -83,7 +120,7 @@ const Dashboard = (props) => {
myChart.setOption(option); myChart.setOption(option);
return ( return (
<Row gutter={80}> <Row id="statistic" gutter={80}>
<Col span={50}> <Col span={50}>
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}> <Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Statistic title={i18next.t("home:Total users")} fontSize="100px" value={dashboardData.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} /> <Statistic title={i18next.t("home:Total users")} fontSize="100px" value={dashboardData.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} />
@@ -112,6 +149,17 @@ const Dashboard = (props) => {
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}> <div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
{renderEChart()} {renderEChart()}
<div id="echarts-chart" style={{width: "80%", height: "400px", textAlign: "center", marginTop: "20px"}} /> <div id="echarts-chart" style={{width: "80%", height: "400px", textAlign: "center", marginTop: "20px"}} />
<Tour
open={isTourVisible}
onClose={setIsTourToLocal}
steps={getSteps()}
indicatorsRender={(current, total) => (
<span>
{current + 1} / {total}
</span>
)}
onFinish={handleTourComplete}
/>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,50 @@
// 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 {Tooltip} from "antd";
import {QuestionCircleOutlined} from "@ant-design/icons";
import * as TourConfig from "../TourConfig";
import * as Setting from "../Setting";
class OpenTour extends React.Component {
constructor(props) {
super(props);
this.state = {
isTourVisible: props.isTourVisible ?? TourConfig.getTourVisible(),
};
}
canTour = () => {
const path = window.location.pathname.replace("/", "");
return TourConfig.TourUrlList.indexOf(path) !== -1 || path === "";
};
render() {
return (
this.canTour() ?
<Tooltip title="Click to enable the help wizard.">
<div className="select-box" style={{display: Setting.isMobile() ? "none" : null, ...this.props.style}} onClick={() => TourConfig.setIsTourVisible(true)} >
<QuestionCircleOutlined style={{fontSize: "24px", color: "#4d4d4d"}} />
</div>
</Tooltip>
:
<div className="select-box" style={{display: Setting.isMobile() ? "none" : null, cursor: "not-allowed", ...this.props.style}} >
<QuestionCircleOutlined style={{fontSize: "24px", color: "#adadad"}} />
</div>
);
}
}
export default OpenTour;

View File

@@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Card, Col, Row} from "antd"; import {Card, Col, Radio, Row} from "antd";
import * as PricingBackend from "../backend/PricingBackend"; import * as PricingBackend from "../backend/PricingBackend";
import * as PlanBackend from "../backend/PlanBackend"; import * as PlanBackend from "../backend/PlanBackend";
import CustomGithubCorner from "../common/CustomGithubCorner"; import CustomGithubCorner from "../common/CustomGithubCorner";
@@ -24,13 +24,17 @@ import i18next from "i18next";
class PricingPage extends React.Component { class PricingPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const params = new URLSearchParams(window.location.search);
this.state = { this.state = {
classes: props, classes: props,
applications: null, applications: null,
owner: props.owner ?? (props.match?.params?.owner ?? null), owner: props.owner ?? (props.match?.params?.owner ?? null),
pricingName: (props.pricingName ?? props.match?.params?.pricingName) ?? null, pricingName: (props.pricingName ?? props.match?.params?.pricingName) ?? null,
userName: params.get("user"),
pricing: props.pricing, pricing: props.pricing,
plans: null, plans: null,
periods: null,
selectedPeriod: null,
loading: false, loading: false,
}; };
} }
@@ -39,7 +43,9 @@ class PricingPage extends React.Component {
this.setState({ this.setState({
applications: [], applications: [],
}); });
if (this.state.userName) {
Setting.showMessage("info", `${i18next.t("pricing:paid-user do not have active subscription or pending subscription, please select a plan to buy")}`);
}
if (this.state.pricing) { if (this.state.pricing) {
this.loadPlans(); this.loadPlans();
} else { } else {
@@ -60,7 +66,7 @@ class PricingPage extends React.Component {
loadPlans() { loadPlans() {
const plans = this.state.pricing.plans.map((plan) => const plans = this.state.pricing.plans.map((plan) =>
PlanBackend.getPlanById(plan, true)); PlanBackend.getPlan(this.state.owner, plan, true));
Promise.all(plans) Promise.all(plans)
.then(results => { .then(results => {
@@ -69,8 +75,12 @@ class PricingPage extends React.Component {
Setting.showMessage("error", i18next.t("pricing:Failed to get plans")); Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
return; return;
} }
const plans = results.map(result => result.data);
const periods = [... new Set(plans.map(plan => plan.period).filter(period => period !== ""))];
this.setState({ this.setState({
plans: results, plans: plans,
periods: periods,
selectedPeriod: periods?.[0],
loading: false, loading: false,
}); });
}) })
@@ -80,17 +90,15 @@ class PricingPage extends React.Component {
} }
loadPricing(pricingName) { loadPricing(pricingName) {
if (pricingName === undefined) { if (!pricingName) {
return; return;
} }
PricingBackend.getPricing(this.state.owner, pricingName) PricingBackend.getPricing(this.state.owner, pricingName)
.then((res) => { .then((res) => {
if (res.status === "error") { if (res.status === "error") {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
return; return;
} }
this.setState({ this.setState({
loading: false, loading: false,
pricing: res.data, pricing: res.data,
@@ -103,11 +111,37 @@ class PricingPage extends React.Component {
this.props.onUpdatePricing(pricing); this.props.onUpdatePricing(pricing);
} }
renderCards() { renderSelectPeriod() {
if (!this.state.periods || this.state.periods.length <= 1) {
return null;
}
return (
<Radio.Group
value={this.state.selectedPeriod}
size="large"
buttonStyle="solid"
onChange={e => {
this.setState({selectedPeriod: e.target.value});
}}
>
{
this.state.periods.map(period => {
return (
<Radio.Button key={period} value={period}>{period}</Radio.Button>
);
})
}
</Radio.Group>
);
}
const getUrlByPlan = (plan) => { renderCards() {
const getUrlByPlan = (planName) => {
const pricing = this.state.pricing; const pricing = this.state.pricing;
const signUpUrl = `/signup/${pricing.application}?plan=${plan}&pricing=${pricing.name}`; let signUpUrl = `/signup/${pricing.application}?plan=${planName}&pricing=${pricing.name}`;
if (this.state.userName) {
signUpUrl = `/buy-plan/${pricing.owner}/${pricing.name}?plan=${planName}&user=${this.state.userName}`;
}
return `${window.location.origin}${signUpUrl}`; return `${window.location.origin}${signUpUrl}`;
}; };
@@ -116,9 +150,9 @@ class PricingPage extends React.Component {
<Card style={{border: "none"}} bodyStyle={{padding: 0}}> <Card style={{border: "none"}} bodyStyle={{padding: 0}}>
{ {
this.state.plans.map(item => { this.state.plans.map(item => {
return ( return item.period === this.state.selectedPeriod ? (
<SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} /> <SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
); ) : null;
}) })
} }
</Card> </Card>
@@ -129,9 +163,9 @@ class PricingPage extends React.Component {
<Row style={{justifyContent: "center"}} gutter={24}> <Row style={{justifyContent: "center"}} gutter={24}>
{ {
this.state.plans.map(item => { this.state.plans.map(item => {
return ( return item.period === this.state.selectedPeriod ? (
<SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} /> <SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
); ) : null;
}) })
} }
</Row> </Row>
@@ -155,6 +189,13 @@ class PricingPage extends React.Component {
<div className="login-form"> <div className="login-form">
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1> <h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
<span style={{fontSize: "20px"}}>{pricing.description}</span> <span style={{fontSize: "20px"}}>{pricing.description}</span>
<Row style={{width: "100%", marginTop: "40px"}}>
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
{
this.renderSelectPeriod()
}
</Col>
</Row>
<Row style={{width: "100%", marginTop: "40px"}}> <Row style={{width: "100%", marginTop: "40px"}}>
<Col span={24} style={{display: "flex", justifyContent: "center"}} > <Col span={24} style={{display: "flex", justifyContent: "center"}} >
{ {

View File

@@ -14,7 +14,7 @@
import i18next from "i18next"; import i18next from "i18next";
import React from "react"; import React from "react";
import {Button, Card, Col} from "antd"; import {Button, Card, Col, Row} from "antd";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
@@ -29,49 +29,49 @@ class SingleCard extends React.Component {
} }
renderCard(plan, isSingle, link) { renderCard(plan, isSingle, link) {
return ( return (
<Col style={{minWidth: "320px", paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px", paddingTop: "0px"}} span={6}> <Col style={{minWidth: "320px", paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px", paddingTop: "0px"}} span={6}>
<Card <Card
hoverable hoverable
onClick={() => Setting.isMobile() ? window.location.href = link : null} onClick={() => Setting.isMobile() ? window.location.href = link : null}
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}} style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
title={<h2>{plan.displayName}</h2>}
> >
<div style={{textAlign: "right"}}> <Col>
<h2 <Row>
style={{marginTop: "0px"}}>{plan.displayName}</h2> <div style={{textAlign: "left"}} className="px-10 mt-5">
</div> <span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.price}</span>
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {plan.period === "Yearly" ? i18next.t("plan:per year") : i18next.t("plan:per month")}</span>
</div>
</Row>
<div style={{textAlign: "left"}} className="px-10 mt-5"> <Row style={{height: "90px", paddingTop: "15px"}}>
<span style={{fontWeight: 700, fontSize: "48px"}}>$ {plan.pricePerMonth}</span> <div style={{textAlign: "left", fontSize: "18px"}}>
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span> <Meta description={plan.description} />
</div> </div>
</Row>
<br /> {/* <ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
<div style={{textAlign: "left", fontSize: "18px"}}> {(plan.options ?? []).map((option) => {
<Meta description={plan.description} /> // eslint-disable-next-line react/jsx-key
</div> return <li>
<br /> <svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg"
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}> viewBox="0 0 20 20">
{(plan.options ?? []).map((option) => { <path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path>
// eslint-disable-next-line react/jsx-key </svg>
return <li> <span style={{fontSize: "16px"}}>{option}</span>
<svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg" </li>;
viewBox="0 0 20 20"> })}
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path> </ul> */}
</svg>
<span style={{fontSize: "16px"}}>{option}</span>
</li>;
})}
</ul>
<div style={{minHeight: "60px"}}>
</div> <Row style={{paddingTop: "15px"}}>
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}> <Button style={{width: "100%", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
{ {
i18next.t("pricing:Getting started") i18next.t("pricing:Getting started")
} }
</Button> </Button>
</Row>
</Col>
</Card> </Card>
</Col> </Col>
); );

View File

@@ -41,7 +41,7 @@ class PolicyTable extends React.Component {
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
if (this.props.mode === "edit") { if (this.props.mode === "edit" && this.props.enforcer.adapter !== "") {
this.getPolicies(); this.getPolicies();
} }
} }
@@ -105,7 +105,7 @@ class PolicyTable extends React.Component {
AdapterBackend.getPolicies(this.props.enforcer.owner, this.props.enforcer.name) AdapterBackend.getPolicies(this.props.enforcer.owner, this.props.enforcer.name)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("adapter:Sync policies successfully")); // Setting.showMessage("success", i18next.t("adapter:Sync policies successfully"));
const policyList = res.data; const policyList = res.data;
policyList.map((policy, index) => { policyList.map((policy, index) => {
@@ -175,7 +175,7 @@ class PolicyTable extends React.Component {
render: (text, record, index) => { render: (text, record, index) => {
const editing = this.isEditing(index); const editing = this.isEditing(index);
return ( return (
editing ? (editing && this.props.modelCfg) ?
<Select size={"small"} style={{width: "60px"}} options={Object.keys(this.props.modelCfg).reverse().map(item => Setting.getOption(item, item))} value={text} onChange={value => { <Select size={"small"} style={{width: "60px"}} options={Object.keys(this.props.modelCfg).reverse().map(item => Setting.getOption(item, item))} value={text} onChange={value => {
this.updateField(table, index, "Ptype", value); this.updateField(table, index, "Ptype", value);
}} /> }} />
@@ -186,7 +186,7 @@ class PolicyTable extends React.Component {
]; ];
const columnKeys = ["V0", "V1", "V2", "V3", "V4", "V5"]; const columnKeys = ["V0", "V1", "V2", "V3", "V4", "V5"];
const columnTitles = this.props.modelCfg["p"].split(","); const columnTitles = this.props.modelCfg ? this.props.modelCfg["p"].split(",") : columnKeys;
columnTitles.forEach((title, i) => { columnTitles.forEach((title, i) => {
columns.push({ columns.push({
title: title, title: title,
@@ -209,7 +209,7 @@ class PolicyTable extends React.Component {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: "", dataIndex: "",
key: "op", key: "op",
width: "130px", width: "150px",
render: (text, record, index) => { render: (text, record, index) => {
const editable = this.isEditing(index); const editable = this.isEditing(index);
return editable ? ( return editable ? (
@@ -247,7 +247,7 @@ class PolicyTable extends React.Component {
loading={this.state.loading} loading={this.state.loading}
title={() => ( title={() => (
<div> <div>
<Button disabled={this.state.editingIndex !== "" || Setting.builtInObject(this.props.enforcer)} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button> <Button disabled={this.state.editingIndex !== "" || this.props.enforcer.model === "" || this.props.enforcer.adapter === "" || Setting.builtInObject(this.props.enforcer)} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div> </div>
)} )}
/> />
@@ -257,7 +257,7 @@ class PolicyTable extends React.Component {
render() { render() {
return ( return (
<React.Fragment> <React.Fragment>
<Button style={{marginBottom: "10px", width: "150px"}} type="primary" disabled={this.state.editingIndex !== ""} onClick={() => {this.getPolicies();}}> <Button disabled={this.state.editingIndex !== "" || this.props.enforcer.model === "" || this.props.enforcer.adapter === ""} style={{marginBottom: "10px", width: "150px"}} type="primary" onClick={() => {this.getPolicies();}}>
{i18next.t("general:Sync")} {i18next.t("general:Sync")}
</Button> </Button>
{ {

View File

@@ -137,7 +137,7 @@ class SignupTable extends React.Component {
} }
return ( return (
<Switch checked={text} onChange={checked => { <Switch checked={text} disabled={record.name === "Password"} onChange={checked => {
this.updateField(table, index, "required", checked); this.updateField(table, index, "required", checked);
}} /> }} />
); );

View File

@@ -96,9 +96,9 @@ class SyncerTableColumnTable extends React.Component {
key: "casdoorName", key: "casdoorName",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Select virtual={false} style={{width: "100%"}} value={text} onChange={(value => {this.updateField(table, index, "casdoorName", value);})}> <Select virtual={false} showSearch style={{width: "100%"}} value={text} onChange={(value => {this.updateField(table, index, "casdoorName", value);})}>
{ {
["Name", "CreatedTime", "UpdatedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar", ["Owner", "Name", "CreatedTime", "UpdatedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region", "Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
"Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp", "Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp",
"PreferredMfaType", "TotpSecret", "SignupApplication"] "PreferredMfaType", "TotpSecret", "SignupApplication"]

View File

@@ -14,13 +14,9 @@
package xlsx package xlsx
import ( import "github.com/tealeg/xlsx"
"github.com/casdoor/casdoor/util"
"github.com/tealeg/xlsx"
)
func ReadXlsxFile(fileId string) [][]string { func ReadXlsxFile(path string) [][]string {
path := util.GetUploadXlsxPath(fileId)
file, err := xlsx.OpenFile(path) file, err := xlsx.OpenFile(path)
if err != nil { if err != nil {
panic(err) panic(err)