mirror of
https://github.com/casdoor/casdoor.git
synced 2025-08-02 02:20:30 +08:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0c5c308071 | ||
![]() |
0b859197da | ||
![]() |
3078409343 | ||
![]() |
bbf2db2e00 | ||
![]() |
0c7b911ce7 | ||
![]() |
2cc55715ac | ||
![]() |
c829bf1769 | ||
![]() |
ec956c12ca | ||
![]() |
d3d4646c56 | ||
![]() |
669ac7c618 | ||
![]() |
6715efd781 | ||
![]() |
953be4a7b6 | ||
![]() |
943cc43427 | ||
![]() |
1e5ce7a045 | ||
![]() |
7a85b74573 | ||
![]() |
7e349c1768 | ||
![]() |
b19be2df88 | ||
![]() |
fc3866db1c | ||
![]() |
bf2bb31e41 | ||
![]() |
ec8bd6f01d | ||
![]() |
98722fd681 | ||
![]() |
221c55aa93 | ||
![]() |
988b26b3c2 | ||
![]() |
7e3c361ce7 | ||
![]() |
a637707e77 | ||
![]() |
7970edeaa7 | ||
![]() |
9da2f0775f | ||
![]() |
739a9bcd0d | ||
![]() |
fb0949b9ed | ||
![]() |
27ed901167 | ||
![]() |
ceab662b88 | ||
![]() |
05b2f00057 | ||
![]() |
8073dfa88c | ||
![]() |
1eeeb64a0c | ||
![]() |
f5e0461cae |
@@ -87,6 +87,7 @@ p, *, *, GET, /api/get-prometheus-info, *, *
|
||||
p, *, *, *, /api/metrics, *, *
|
||||
p, *, *, GET, /api/get-pricing, *, *
|
||||
p, *, *, GET, /api/get-plan, *, *
|
||||
p, *, *, GET, /api/get-subscription, *, *
|
||||
p, *, *, GET, /api/get-organization-names, *, *
|
||||
`
|
||||
|
||||
|
@@ -140,25 +140,28 @@ func (c *ApiController) Signup() {
|
||||
username = id
|
||||
}
|
||||
|
||||
password := authForm.Password
|
||||
msg = object.CheckPasswordComplexityByOrg(organization, password)
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
|
||||
initScore, err := organization.GetInitScore()
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
|
||||
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{
|
||||
Owner: authForm.Organization,
|
||||
Name: username,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Id: id,
|
||||
Type: "normal-user",
|
||||
Type: userType,
|
||||
Password: authForm.Password,
|
||||
DisplayName: authForm.Name,
|
||||
Avatar: organization.DefaultAvatar,
|
||||
@@ -210,7 +213,7 @@ func (c *ApiController) Signup() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.HasPromptPage() {
|
||||
if application.HasPromptPage() && user.Type == "normal-user" {
|
||||
// The prompt page needs the user to be signed in
|
||||
c.SetSessionUsername(user.GetId())
|
||||
}
|
||||
@@ -227,15 +230,6 @@ func (c *ApiController) Signup() {
|
||||
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.Organization = application.Organization
|
||||
record.User = user.Name
|
||||
|
@@ -62,13 +62,13 @@ func (c *ApiController) GetApplications() {
|
||||
}
|
||||
|
||||
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 {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
applications := object.GetMaskedApplications(app, userId)
|
||||
applications := object.GetMaskedApplications(application, userId)
|
||||
c.ResponseOk(applications, paginator.Nums())
|
||||
}
|
||||
}
|
||||
@@ -84,13 +84,23 @@ func (c *ApiController) GetApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
|
||||
app, err := object.GetApplication(id)
|
||||
application, err := object.GetApplication(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
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
|
||||
@@ -164,13 +174,13 @@ func (c *ApiController) GetOrganizationApplications() {
|
||||
}
|
||||
|
||||
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 {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
applications := object.GetMaskedApplications(app, userId)
|
||||
applications := object.GetMaskedApplications(application, userId)
|
||||
c.ResponseOk(applications, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
c.SetSessionUsername(userId)
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
|
@@ -61,6 +61,10 @@ func (c *ApiController) IsAdminOrSelf(user2 *object.User) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if user == nil || user2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Owner == user2.Owner && user.Name == user2.Name {
|
||||
return true
|
||||
}
|
||||
|
@@ -35,6 +35,11 @@ const (
|
||||
UnauthorizedService string = "UNAUTHORIZED_SERVICE"
|
||||
)
|
||||
|
||||
func queryUnescape(service string) string {
|
||||
s, _ := url.QueryUnescape(service)
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *RootController) CasValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
service := c.Input().Get("service")
|
||||
@@ -60,24 +65,25 @@ func (c *RootController) CasServiceValidate() {
|
||||
if !strings.HasPrefix(ticket, "ST") {
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
}
|
||||
c.CasP3ServiceAndProxyValidate()
|
||||
c.CasP3ProxyValidate()
|
||||
}
|
||||
|
||||
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")
|
||||
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.CasP3ServiceAndProxyValidate()
|
||||
c.CasP3ProxyValidate()
|
||||
}
|
||||
|
||||
func queryUnescape(service string) string {
|
||||
s, _ := url.QueryUnescape(service)
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *RootController) CasP3ServiceAndProxyValidate() {
|
||||
func (c *RootController) CasP3ProxyValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
service := c.Input().Get("service")
|
||||
@@ -115,15 +121,17 @@ func (c *RootController) CasP3ServiceAndProxyValidate() {
|
||||
pgtiou := serviceResponse.Success.ProxyGrantingTicket
|
||||
// todo: check whether it is https
|
||||
pgtUrlObj, err := url.Parse(pgtUrl)
|
||||
if err != nil {
|
||||
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, err.Error(), format)
|
||||
return
|
||||
}
|
||||
|
||||
if pgtUrlObj.Scheme != "https" {
|
||||
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, "callback is not https", format)
|
||||
return
|
||||
}
|
||||
|
||||
// make a request to pgturl passing pgt and pgtiou
|
||||
if err != nil {
|
||||
c.sendCasAuthenticationResponseErr(InternalError, err.Error(), format)
|
||||
return
|
||||
}
|
||||
param := pgtUrlObj.Query()
|
||||
param.Add("pgtId", pgt)
|
||||
param.Add("pgtIou", pgtiou)
|
||||
@@ -263,7 +271,6 @@ func (c *RootController) sendCasAuthenticationResponseErr(code, msg, format stri
|
||||
Message: msg,
|
||||
},
|
||||
}
|
||||
|
||||
if format == "json" {
|
||||
c.Data["json"] = serviceResponse
|
||||
c.ServeJSON()
|
||||
|
@@ -83,7 +83,7 @@ func (c *ApiController) GetEnforcer() {
|
||||
return
|
||||
}
|
||||
|
||||
if loadModelCfg == "true" {
|
||||
if loadModelCfg == "true" && enforcer.Model != "" {
|
||||
err := enforcer.LoadModelCfg()
|
||||
if err != nil {
|
||||
return
|
||||
|
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"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))
|
||||
|
||||
path := util.GetUploadXlsxPath(fileId)
|
||||
util.EnsureFileFolderExists(path)
|
||||
defer os.Remove(path)
|
||||
err = saveFile(path, &file)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := object.UploadPermissions(owner, fileId)
|
||||
affected, err := object.UploadPermissions(owner, path)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -82,7 +83,10 @@ func (c *ApiController) GetPlan() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if plan == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("plan:The plan: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
if includeOption {
|
||||
options, err := object.GetPermissionsByRole(plan.Role)
|
||||
if err != nil {
|
||||
@@ -110,14 +114,29 @@ func (c *ApiController) GetPlan() {
|
||||
// @router /update-plan [post]
|
||||
func (c *ApiController) UpdatePlan() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
owner := util.GetOwnerFromId(id)
|
||||
var plan object.Plan
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
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.ServeJSON()
|
||||
}
|
||||
@@ -136,7 +155,14 @@ func (c *ApiController) AddPlan() {
|
||||
c.ResponseError(err.Error())
|
||||
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.ServeJSON()
|
||||
}
|
||||
@@ -155,7 +181,13 @@ func (c *ApiController) DeletePlan() {
|
||||
c.ResponseError(err.Error())
|
||||
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.ServeJSON()
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -80,7 +81,10 @@ func (c *ApiController) GetPricing() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if pricing == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("pricing:The pricing: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
c.ResponseOk(pricing)
|
||||
}
|
||||
|
||||
|
@@ -161,10 +161,17 @@ func (c *ApiController) DeleteProduct() {
|
||||
// @router /buy-product [post]
|
||||
func (c *ApiController) BuyProduct() {
|
||||
id := c.Input().Get("id")
|
||||
providerName := c.Input().Get("providerName")
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
userId := c.GetSessionUsername()
|
||||
providerName := c.Input().Get("providerName")
|
||||
// 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 == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
@@ -175,13 +182,12 @@ func (c *ApiController) BuyProduct() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
payUrl, orderId, err := object.BuyProduct(id, providerName, user, host)
|
||||
payUrl, orderId, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"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))
|
||||
|
||||
path := util.GetUploadXlsxPath(fileId)
|
||||
util.EnsureFileFolderExists(path)
|
||||
defer os.Remove(path)
|
||||
err = saveFile(path, &file)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := object.UploadRoles(owner, fileId)
|
||||
affected, err := object.UploadRoles(owner, path)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
|
@@ -160,7 +160,11 @@ func (c *ApiController) RunSyncer() {
|
||||
return
|
||||
}
|
||||
|
||||
object.RunSyncer(syncer)
|
||||
err = object.RunSyncer(syncer)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
@@ -47,19 +47,16 @@ func (c *ApiController) GetSystemInfo() {
|
||||
// @router /get-version-info [get]
|
||||
func (c *ApiController) GetVersionInfo() {
|
||||
versionInfo, err := util.GetVersionInfo()
|
||||
if versionInfo.Version != "" {
|
||||
c.ResponseOk(versionInfo)
|
||||
return
|
||||
}
|
||||
|
||||
versionInfo, err = util.GetVersionInfoFromFile()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if versionInfo.Version == "" {
|
||||
versionInfo, err = util.GetVersionInfoFromFile()
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.ResponseOk(versionInfo)
|
||||
}
|
||||
|
||||
|
@@ -48,17 +48,17 @@ func (c *ApiController) UploadUsers() {
|
||||
c.ResponseError(err.Error())
|
||||
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)
|
||||
util.EnsureFileFolderExists(path)
|
||||
defer os.Remove(path)
|
||||
err = saveFile(path, &file)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := object.UploadUsers(owner, fileId)
|
||||
affected, err := object.UploadUsers(owner, path)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
23
form/auth.go
23
form/auth.go
@@ -17,17 +17,18 @@ package form
|
||||
type AuthForm struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
Organization string `json:"organization"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Affiliation string `json:"affiliation"`
|
||||
IdCard string `json:"idCard"`
|
||||
Region string `json:"region"`
|
||||
Organization string `json:"organization"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Affiliation string `json:"affiliation"`
|
||||
IdCard string `json:"idCard"`
|
||||
Region string `json:"region"`
|
||||
InvitationCode string `json:"invitationCode"`
|
||||
|
||||
Application string `json:"application"`
|
||||
ClientId string `json:"clientId"`
|
||||
|
@@ -151,7 +151,7 @@ func (adapter *Adapter) InitAdapter() error {
|
||||
if adapter.Adapter == nil {
|
||||
var dataSourceName string
|
||||
|
||||
if adapter.builtInAdapter() {
|
||||
if adapter.isBuiltIn() {
|
||||
dataSourceName = conf.GetConfigString("dataSourceName")
|
||||
if adapter.DatabaseType == "mysql" {
|
||||
dataSourceName = dataSourceName + adapter.Database
|
||||
@@ -183,6 +183,14 @@ func (adapter *Adapter) InitAdapter() error {
|
||||
|
||||
var err error
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -211,7 +219,7 @@ func adapterChangeTrigger(oldName string, newName string) error {
|
||||
return session.Commit()
|
||||
}
|
||||
|
||||
func (adapter *Adapter) builtInAdapter() bool {
|
||||
func (adapter *Adapter) isBuiltIn() bool {
|
||||
if adapter.Owner != "built-in" {
|
||||
return false
|
||||
}
|
||||
|
@@ -57,7 +57,9 @@ type Application struct {
|
||||
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
|
||||
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
||||
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
|
||||
CertPublicKey string `xorm:"-" json:"certPublicKey"`
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"`
|
||||
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
@@ -311,6 +313,11 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
||||
application.OrganizationObj.PasswordSalt = "***"
|
||||
}
|
||||
}
|
||||
|
||||
if application.InvitationCodes != nil {
|
||||
application.InvitationCodes = []string{"***"}
|
||||
}
|
||||
|
||||
return application
|
||||
}
|
||||
|
||||
|
@@ -66,8 +66,11 @@ func CheckUserSignup(application *Application, organization *Organization, form
|
||||
}
|
||||
}
|
||||
|
||||
if len(form.Password) <= 5 {
|
||||
return i18n.Translate(lang, "check:Password must have at least 6 characters")
|
||||
if application.IsSignupItemVisible("Password") {
|
||||
msg := CheckPasswordComplexityByOrg(organization, form.Password)
|
||||
if msg != "" {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
if application.IsSignupItemVisible("Email") {
|
||||
@@ -124,6 +127,18 @@ func CheckUserSignup(application *Application, organization *Organization, form
|
||||
}
|
||||
}
|
||||
|
||||
if len(application.InvitationCodes) > 0 {
|
||||
if form.InvitationCode == "" {
|
||||
if application.IsSignupItemRequired("Invitation code") {
|
||||
return i18n.Translate(lang, "check:Invitation code cannot be blank")
|
||||
}
|
||||
} else {
|
||||
if !util.InSlice(application.InvitationCodes, form.InvitationCode) {
|
||||
return i18n.Translate(lang, "check:Invitation code is invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -404,7 +419,7 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@@ -18,11 +18,14 @@ import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
xormadapter "github.com/casdoor/xorm-adapter/v3"
|
||||
_ "github.com/denisenkom/go-mssqldb" // db = mssql
|
||||
_ "github.com/go-sql-driver/mysql" // db = mysql
|
||||
@@ -65,6 +68,17 @@ func InitConfig() {
|
||||
}
|
||||
|
||||
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 {
|
||||
err := createDatabaseForPostgres(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
|
||||
if err != nil {
|
||||
@@ -122,9 +136,14 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) *Ormer
|
||||
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 {
|
||||
if driverName == "postgres" {
|
||||
db, err := sql.Open(driverName, dataSourceName)
|
||||
db, err := sql.Open(driverName, refineDataSourceNameForPostgres(dataSourceName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -136,6 +155,21 @@ func createDatabaseForPostgres(driverName string, dataSourceName string, dbName
|
||||
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
|
||||
} else {
|
||||
@@ -168,6 +202,12 @@ func (a *Ormer) open() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if a.driverName == "postgres" {
|
||||
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
|
||||
if schema != "" {
|
||||
engine.SetSchema(schema)
|
||||
}
|
||||
}
|
||||
|
||||
a.Engine = engine
|
||||
}
|
||||
|
@@ -79,9 +79,7 @@ func (p *Permission) setEnforcerAdapter(enforcer *casbin.Enforcer) error {
|
||||
}
|
||||
}
|
||||
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
||||
driverName := conf.GetConfigString("driverName")
|
||||
dataSourceName := conf.GetConfigRealDataSourceName(driverName)
|
||||
adapter, err := xormadapter.NewAdapterWithTableName(driverName, dataSourceName, tableName, tableNamePrefix, true)
|
||||
adapter, err := xormadapter.NewAdapterByEngineWithTableName(ormer.Engine, tableName, tableNamePrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -33,8 +33,8 @@ func getPermissionMap(owner string) (map[string]*Permission, error) {
|
||||
return m, err
|
||||
}
|
||||
|
||||
func UploadPermissions(owner string, fileId string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(fileId)
|
||||
func UploadPermissions(owner string, path string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(path)
|
||||
|
||||
oldUserMap, err := getPermissionMap(owner)
|
||||
if err != nil {
|
||||
|
@@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@@ -28,15 +29,39 @@ type Plan struct {
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
|
||||
PricePerMonth float64 `json:"pricePerMonth"`
|
||||
PricePerYear float64 `json:"pricePerYear"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
Price float64 `json:"price"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Period string `xorm:"varchar(100)" json:"period"`
|
||||
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"`
|
||||
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) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Plan{})
|
||||
@@ -114,38 +139,3 @@ func DeletePlan(plan *Plan) (bool, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@@ -16,7 +16,6 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@@ -33,12 +32,26 @@ type Pricing struct {
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
TrialDuration int `json:"trialDuration"`
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
}
|
||||
|
||||
Submitter string `xorm:"varchar(100)" json:"submitter"`
|
||||
Approver string `xorm:"varchar(100)" json:"approver"`
|
||||
ApproveTime string `xorm:"varchar(100)" json:"approveTime"`
|
||||
func (pricing *Pricing) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", pricing.Owner, pricing.Name)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -74,7 +87,7 @@ func getPricing(owner, name string) (*Pricing, error) {
|
||||
pricing := Pricing{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&pricing)
|
||||
if err != nil {
|
||||
return &pricing, err
|
||||
return nil, err
|
||||
}
|
||||
if existed {
|
||||
return &pricing, nil
|
||||
@@ -88,6 +101,20 @@ func GetPricing(id string) (*Pricing, error) {
|
||||
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) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if p, err := getPricing(owner, name); err != nil {
|
||||
@@ -120,28 +147,21 @@ func DeletePricing(pricing *Pricing) (bool, error) {
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (pricing *Pricing) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", pricing.Owner, pricing.Name)
|
||||
}
|
||||
|
||||
func (pricing *Pricing) HasPlan(owner string, plan string) (bool, error) {
|
||||
selectedPlan, err := GetPlan(fmt.Sprintf("%s/%s", owner, plan))
|
||||
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
|
||||
func CheckPricingAndPlan(owner, pricingName, planName string) error {
|
||||
pricingId := util.GetId(owner, pricingName)
|
||||
pricing, err := GetPricing(pricingId)
|
||||
if pricing == nil || err != nil {
|
||||
if pricing == nil && err == nil {
|
||||
err = fmt.Errorf("pricing: %s does not exist", pricingName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
ok, err := pricing.HasPlan(planName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("pricing: %s does not have plan: %s", pricingName, planName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -141,24 +141,24 @@ func (product *Product) isValidProvider(provider *Provider) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (product *Product) getProvider(providerId string) (*Provider, error) {
|
||||
provider, err := getProvider(product.Owner, providerId)
|
||||
func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||
provider, err := getProvider(product.Owner, providerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -181,13 +181,31 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
|
||||
owner := product.Owner
|
||||
productName := product.Name
|
||||
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
||||
paymentName := util.GenerateTimeId()
|
||||
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
|
||||
productDisplayName := product.DisplayName
|
||||
|
||||
originFrontend, originBackend := getOriginFromHost(host)
|
||||
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, 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)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -228,7 +246,6 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
|
||||
if !affected {
|
||||
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
||||
}
|
||||
|
||||
return payUrl, orderId, err
|
||||
}
|
||||
|
||||
@@ -252,3 +269,38 @@ func ExtendProductWithProviders(product *Product) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
@@ -33,8 +33,8 @@ func getRoleMap(owner string) (map[string]*Role, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func UploadRoles(owner string, fileId string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(fileId)
|
||||
func UploadRoles(owner string, path string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(path)
|
||||
|
||||
oldUserMap, err := getRoleMap(owner)
|
||||
if err != nil {
|
||||
|
@@ -18,47 +18,108 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"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 {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
|
||||
StartDate time.Time `json:"startDate"`
|
||||
EndDate time.Time `json:"endDate"`
|
||||
Duration int `json:"duration"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
Pricing string `xorm:"varchar(100)" json:"pricing"`
|
||||
Plan string `xorm:"varchar(100)" json:"plan"`
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
|
||||
User string `xorm:"mediumtext" json:"user"`
|
||||
Plan string `xorm:"varchar(100)" json:"plan"`
|
||||
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
Submitter string `xorm:"varchar(100)" json:"submitter"`
|
||||
Approver string `xorm:"varchar(100)" json:"approver"`
|
||||
ApproveTime string `xorm:"varchar(100)" json:"approveTime"`
|
||||
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
Period string `xorm:"varchar(100)" json:"period"`
|
||||
State SubscriptionState `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]
|
||||
return &Subscription{
|
||||
Name: "Subscription_" + id,
|
||||
DisplayName: "New Subscription - " + id,
|
||||
Owner: owner,
|
||||
User: owner + "/" + user,
|
||||
Plan: owner + "/" + plan,
|
||||
Name: "sub_" + id,
|
||||
DisplayName: "New Subscription - " + id,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
State: defaultStatus,
|
||||
Duration: duration,
|
||||
StartDate: time.Now(),
|
||||
EndDate: time.Now().AddDate(0, 0, duration),
|
||||
|
||||
User: userName,
|
||||
Plan: planName,
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -84,7 +166,12 @@ func GetPaginationSubscriptions(owner string, offset, limit int, field, value, s
|
||||
if err != nil {
|
||||
return subscriptions, err
|
||||
}
|
||||
|
||||
for _, sub := range subscriptions {
|
||||
err = sub.UpdateState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
@@ -144,7 +231,3 @@ func DeleteSubscription(subscription *Subscription) (bool, error) {
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (subscription *Subscription) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", subscription.Owner, subscription.Name)
|
||||
}
|
||||
|
@@ -37,12 +37,13 @@ type Syncer struct {
|
||||
|
||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||
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"`
|
||||
Port int `json:"port"`
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
Password string `xorm:"varchar(100)" json:"password"`
|
||||
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
|
||||
Database string `xorm:"varchar(100)" json:"database"`
|
||||
Table string `xorm:"varchar(100)" json:"table"`
|
||||
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
|
||||
@@ -250,7 +251,7 @@ func (syncer *Syncer) getKey() string {
|
||||
return key
|
||||
}
|
||||
|
||||
func RunSyncer(syncer *Syncer) {
|
||||
func RunSyncer(syncer *Syncer) error {
|
||||
syncer.initAdapter()
|
||||
syncer.syncUsers()
|
||||
return syncer.syncUsers()
|
||||
}
|
||||
|
@@ -52,11 +52,14 @@ func addSyncerJob(syncer *Syncer) error {
|
||||
|
||||
syncer.initAdapter()
|
||||
|
||||
syncer.syncUsers()
|
||||
err := syncer.syncUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
|
||||
cron := getCronMap(syncer.Name)
|
||||
_, err := cron.AddFunc(schedule, syncer.syncUsers)
|
||||
_, err = cron.AddFunc(schedule, syncer.syncUsersNoError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -19,15 +19,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (syncer *Syncer) syncUsers() {
|
||||
func (syncer *Syncer) syncUsers() error {
|
||||
if len(syncer.TableColumns) == 0 {
|
||||
return
|
||||
return fmt.Errorf("The syncer table columns should not be empty")
|
||||
}
|
||||
|
||||
fmt.Printf("Running syncUsers()..\n")
|
||||
|
||||
users, _, _ := syncer.getUserMap()
|
||||
oUsers, oUserMap, err := syncer.getOriginalUserMap()
|
||||
oUsers, _, err := syncer.getOriginalUserMap()
|
||||
if err != nil {
|
||||
fmt.Printf(err.Error())
|
||||
|
||||
@@ -35,10 +35,8 @@ func (syncer *Syncer) syncUsers() {
|
||||
line := fmt.Sprintf("[%s] %s\n", timestamp, err.Error())
|
||||
_, err = updateSyncerErrorText(syncer, line)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
myOUsers := map[string]*User{}
|
||||
for _, m := range oUsers {
|
||||
myOUsers[syncer.getUserValue(m, key)] = m
|
||||
}
|
||||
|
||||
newUsers := []*User{}
|
||||
for _, oUser := range oUsers {
|
||||
primary := syncer.getUserValue(oUser, key)
|
||||
@@ -71,28 +74,30 @@ func (syncer *Syncer) syncUsers() {
|
||||
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
||||
updatedUser.Hash = oHash
|
||||
updatedUser.PreHash = oHash
|
||||
|
||||
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
|
||||
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
|
||||
}
|
||||
} else {
|
||||
if user.PreHash == oHash {
|
||||
if !syncer.IsReadOnly {
|
||||
updatedOUser := syncer.createOriginalUserFromUser(user)
|
||||
|
||||
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
|
||||
_, err = syncer.updateUser(updatedOUser)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
|
||||
}
|
||||
|
||||
// update preHash
|
||||
user.PreHash = user.Hash
|
||||
_, err = SetUserField(user, "pre_hash", user.PreHash)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if user.Hash == oHash {
|
||||
@@ -100,17 +105,18 @@ func (syncer *Syncer) syncUsers() {
|
||||
user.PreHash = user.Hash
|
||||
_, err = SetUserField(user, "pre_hash", user.PreHash)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
||||
updatedUser.Hash = oHash
|
||||
updatedUser.PreHash = oHash
|
||||
|
||||
fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser)
|
||||
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
|
||||
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)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !syncer.IsReadOnly {
|
||||
for _, user := range users {
|
||||
id := user.Id
|
||||
if _, ok := oUserMap[id]; !ok {
|
||||
primary := syncer.getUserValue(user, key)
|
||||
if _, ok := myOUsers[primary]; !ok {
|
||||
newOUser := syncer.createOriginalUserFromUser(user)
|
||||
|
||||
fmt.Printf("New oUser: %v\n", newOUser)
|
||||
_, err = syncer.addUser(newOUser)
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@@ -31,8 +31,8 @@ type Credential struct {
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
|
||||
sql := fmt.Sprintf("select * from %s", syncer.getTable())
|
||||
results, err := syncer.Ormer.Engine.QueryString(sql)
|
||||
var results []map[string]string
|
||||
err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,19 +64,10 @@ func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*Origina
|
||||
|
||||
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
|
||||
m := syncer.getMapFromOriginalUser(user)
|
||||
keyString, valueString := syncer.getSqlKeyValueStringFromMap(m)
|
||||
|
||||
sql := fmt.Sprintf("insert into %s (%s) values (%s)", syncer.getTable(), keyString, valueString)
|
||||
res, err := syncer.Ormer.Engine.Exec(sql)
|
||||
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
@@ -93,23 +84,14 @@ func (syncer *Syncer) getCasdoorColumns() []string {
|
||||
|
||||
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
|
||||
key := syncer.getKey()
|
||||
|
||||
m := syncer.getMapFromOriginalUser(user)
|
||||
pkValue := m[key]
|
||||
delete(m, key)
|
||||
setString := syncer.getSqlSetStringFromMap(m)
|
||||
|
||||
sql := fmt.Sprintf("update %s set %s where %s = %s", syncer.getTable(), setString, key, pkValue)
|
||||
res, err := syncer.Ormer.Engine.Exec(sql)
|
||||
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).ID(pkValue).Update(&m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
@@ -185,7 +167,11 @@ func (syncer *Syncer) initAdapter() {
|
||||
if syncer.DatabaseType == "mssql" {
|
||||
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" {
|
||||
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 {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
|
||||
}
|
||||
|
@@ -322,19 +322,3 @@ func (syncer *Syncer) getSqlSetStringFromMap(m map[string]string) string {
|
||||
}
|
||||
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, ", ")
|
||||
}
|
||||
|
@@ -541,7 +541,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
}
|
||||
}
|
||||
if isAdmin {
|
||||
columns = append(columns, "name", "email", "phone", "country_code")
|
||||
columns = append(columns, "name", "email", "phone", "country_code", "type")
|
||||
}
|
||||
|
||||
if util.ContainsString(columns, "groups") {
|
||||
@@ -627,12 +627,10 @@ func AddUser(user *User) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if user.PasswordType == "" && organization.PasswordType != "" {
|
||||
user.PasswordType = organization.PasswordType
|
||||
if user.PasswordType == "" || user.PasswordType == "plain" {
|
||||
user.UpdateUserPassword(organization)
|
||||
}
|
||||
|
||||
user.UpdateUserPassword(organization)
|
||||
|
||||
err = user.UpdateUserHash()
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/errors"
|
||||
"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) {
|
||||
err := e.checkModel()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return e.enforcer.AddRoleForUser(user, GetGroupWithPrefix(group))
|
||||
}
|
||||
|
||||
func (e *UserGroupEnforcer) AddGroupsForUser(user string, groups []string) (bool, error) {
|
||||
err := e.checkModel()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
g := make([]string, len(groups))
|
||||
for i, group := range groups {
|
||||
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) {
|
||||
err := e.checkModel()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return e.enforcer.DeleteRoleForUser(user, GetGroupWithPrefix(group))
|
||||
}
|
||||
|
||||
func (e *UserGroupEnforcer) DeleteGroupsForUser(user string) (bool, error) {
|
||||
err := e.checkModel()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return e.enforcer.DeleteRolesForUser(user)
|
||||
}
|
||||
|
||||
func (e *UserGroupEnforcer) GetGroupsForUser(user string) ([]string, error) {
|
||||
err := e.checkModel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups, err := e.enforcer.GetRolesForUser(user)
|
||||
for i, group := range groups {
|
||||
groups[i] = GetGroupWithoutPrefix(group)
|
||||
@@ -46,6 +80,11 @@ func (e *UserGroupEnforcer) GetGroupsForUser(user 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))
|
||||
if err != nil {
|
||||
if err == errors.ERR_NAME_NOT_FOUND {
|
||||
@@ -65,13 +104,17 @@ func GetGroupWithoutPrefix(group string) string {
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := []string{}
|
||||
for _, userId := range userIds {
|
||||
_, name := util.GetOwnerAndNameFromIdNoCheck(userId)
|
||||
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) {
|
||||
_, err := e.DeleteGroupsForUser(user)
|
||||
err := e.checkModel()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = e.DeleteGroupsForUser(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@@ -73,8 +73,8 @@ func parseListItem(lines *[]string, i int) []string {
|
||||
return trimmedItems
|
||||
}
|
||||
|
||||
func UploadUsers(owner string, fileId string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(fileId)
|
||||
func UploadUsers(owner string, path string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(path)
|
||||
|
||||
oldUserMap, err := getUserMap(owner)
|
||||
if err != nil {
|
||||
|
@@ -14,10 +14,7 @@
|
||||
|
||||
package pp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
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) {
|
||||
payUrl := fmt.Sprintf("/payments/%s/result", paymentName)
|
||||
return payUrl, "", nil
|
||||
return returnUrl, "", nil
|
||||
}
|
||||
|
||||
func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
|
||||
|
@@ -114,8 +114,8 @@ func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, auth
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if captureRsp.Code != paypal.Success {
|
||||
errDetail := captureRsp.ErrorResponse.Details[0]
|
||||
if detailRsp.Code != paypal.Success {
|
||||
errDetail := detailRsp.ErrorResponse.Details[0]
|
||||
switch errDetail.Issue {
|
||||
case "ORDER_NOT_APPROVED":
|
||||
notifyResult.PaymentStatus = PaymentStateCanceled
|
||||
|
@@ -14,9 +14,7 @@
|
||||
|
||||
package pp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
type PaymentState string
|
||||
|
||||
|
@@ -273,7 +273,7 @@ func initAPI() {
|
||||
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/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/p3/proxyValidate", &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:CasP3ProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
|
||||
}
|
||||
|
10
util/path.go
10
util/path.go
@@ -34,16 +34,6 @@ func GetPath(path string) string {
|
||||
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 {
|
||||
res := []string{}
|
||||
|
||||
|
@@ -14,8 +14,13 @@
|
||||
|
||||
package util
|
||||
|
||||
import "fmt"
|
||||
import "io/ioutil"
|
||||
|
||||
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()
|
||||
}
|
||||
|
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
@@ -23,6 +23,7 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -315,3 +316,13 @@ func ParseIdToString(input interface{}) (string, error) {
|
||||
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 ""
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PORT=7001 craco start",
|
||||
"build": "craco --max_old_space_size=4096 build",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject",
|
||||
"crowdin:sync": "crowdin upload && crowdin download",
|
||||
|
@@ -89,6 +89,7 @@ import ThemeSelect from "./common/select/ThemeSelect";
|
||||
import OrganizationSelect from "./common/select/OrganizationSelect";
|
||||
import {clearWeb3AuthToken} from "./auth/Web3Auth";
|
||||
import AccountAvatar from "./account/AccountAvatar";
|
||||
import OpenTour from "./common/OpenTour";
|
||||
|
||||
const {Header, Footer, Content} = Layout;
|
||||
|
||||
@@ -379,6 +380,7 @@ class App extends Component {
|
||||
});
|
||||
}} />
|
||||
<LanguageSelect languages={this.state.account.organization.languages} />
|
||||
<OpenTour />
|
||||
{Setting.isAdminUser(this.state.account) && !Setting.isMobile() &&
|
||||
<OrganizationSelect
|
||||
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 />, [
|
||||
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"),
|
||||
]));
|
||||
|
||||
@@ -460,11 +462,17 @@ class App extends Component {
|
||||
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 />, [
|
||||
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
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")]));
|
||||
if (Setting.isAdminUser(this.state.account)) {
|
||||
res.push(Setting.getItem(<Link style={{color: "black"}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone />, [
|
||||
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
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;
|
||||
@@ -563,9 +571,7 @@ class App extends Component {
|
||||
|
||||
renderContent() {
|
||||
const onClick = ({key}) => {
|
||||
if (key === "/swagger") {
|
||||
window.open(Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger", "_blank");
|
||||
} else {
|
||||
if (key !== "/swagger" && key !== "/records") {
|
||||
if (this.state.requiredEnableMfa) {
|
||||
Setting.showMessage("info", "Please enable MFA first!");
|
||||
} else {
|
||||
@@ -657,7 +663,8 @@ class App extends Component {
|
||||
window.location.pathname.startsWith("/result") ||
|
||||
window.location.pathname.startsWith("/cas") ||
|
||||
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() {
|
||||
|
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, ConfigProvider, Input, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
|
||||
import {Button, Card, Col, ConfigProvider, Input, List, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
|
||||
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
@@ -138,6 +138,10 @@ class ApplicationEditPage extends React.Component {
|
||||
application.tags = [];
|
||||
}
|
||||
|
||||
if (application.invitationCodes === null) {
|
||||
application.invitationCodes = [];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
@@ -201,7 +205,6 @@ class ApplicationEditPage extends React.Component {
|
||||
|
||||
updateApplicationField(key, value) {
|
||||
value = this.parseApplicationField(key, value);
|
||||
|
||||
const application = this.state.application;
|
||||
application[key] = value;
|
||||
this.setState({
|
||||
@@ -813,18 +816,68 @@ class ApplicationEditPage extends React.Component {
|
||||
</Row>
|
||||
{
|
||||
!this.state.application.enableSignUp ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Signup items"), i18next.t("application:Signup items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<SignupTable
|
||||
title={i18next.t("application:Signup items")}
|
||||
table={this.state.application.signupItems}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("signupItems", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Signup items"), i18next.t("application:Signup items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<SignupTable
|
||||
title={i18next.t("application:Signup items")}
|
||||
table={this.state.application.signupItems}
|
||||
onUpdateTable={(value) => {
|
||||
this.updateApplicationField("signupItems", value);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Invitation code"), i18next.t("application:Invitation code - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<List
|
||||
header={
|
||||
<Button type="primary" onClick={() => {
|
||||
this.updateApplicationField("invitationCodes", Setting.addRow(this.state.application.invitationCodes, Setting.getRandomName()));
|
||||
}
|
||||
}>
|
||||
{i18next.t("general:Add")}
|
||||
</Button>
|
||||
}
|
||||
dataSource={this.state.application.invitationCodes.map(code => {
|
||||
return {code: code};
|
||||
})}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item key={index}>
|
||||
<Space>
|
||||
<Input value={item.code} onChange={e => {
|
||||
const invitationCodes = [...this.state.application.invitationCodes];
|
||||
invitationCodes[index] = e.target.value;
|
||||
this.updateApplicationField("invitationCodes", invitationCodes);
|
||||
}} />
|
||||
</Space>
|
||||
<Space>
|
||||
<Button icon={<CopyOutlined />} onClick={() => {
|
||||
copy(item.code);
|
||||
Setting.showMessage("success", i18next.t("application:Invitation code copied to clipboard successfully"));
|
||||
}
|
||||
}>
|
||||
{i18next.t("general:Copy")}
|
||||
</Button>
|
||||
<Button type="primary" danger onClick={() => {
|
||||
this.updateApplicationField("invitationCodes", this.state.application.invitationCodes.filter(code => code !== item.code));
|
||||
}
|
||||
}>
|
||||
{i18next.t("general:Delete")}
|
||||
</Button>
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
@@ -13,11 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
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 Highlighter from "react-highlight-words";
|
||||
import i18next from "i18next";
|
||||
import * as Setting from "./Setting";
|
||||
import * as TourConfig from "./TourConfig";
|
||||
|
||||
class BaseListPage extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -33,6 +34,7 @@ class BaseListPage extends React.Component {
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
isAuthorized: true,
|
||||
isTourVisible: TourConfig.getTourVisible(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,14 +43,23 @@ class BaseListPage extends React.Component {
|
||||
this.fetch({pagination});
|
||||
};
|
||||
|
||||
handleTourChange = () => {
|
||||
this.setState({isTourVisible: TourConfig.getTourVisible()});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("storageOrganizationChanged", this.handleOrganizationChange);
|
||||
window.addEventListener("storageTourChanged", this.handleTourChange);
|
||||
if (!Setting.isAdminUser(this.props.account)) {
|
||||
Setting.setOrganization("All");
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.intervalId !== null) {
|
||||
clearInterval(this.state.intervalId);
|
||||
}
|
||||
window.removeEventListener("storageTourChanged", this.handleTourChange);
|
||||
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() {
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
@@ -161,6 +203,17 @@ class BaseListPage extends React.Component {
|
||||
{
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@@ -14,6 +14,8 @@
|
||||
|
||||
export const DefaultApplication = "app-built-in";
|
||||
|
||||
export const CasvisorUrl = "https://github.com/casbin/casvisor";
|
||||
|
||||
export const ShowGithubCorner = false;
|
||||
export const IsDemoMode = false;
|
||||
|
||||
|
@@ -29,6 +29,8 @@ import PromptPage from "./auth/PromptPage";
|
||||
import ResultPage from "./auth/ResultPage";
|
||||
import CasLogout from "./auth/CasLogout";
|
||||
import {authConfig} from "./auth/Auth";
|
||||
import ProductBuyPage from "./ProductBuyPage";
|
||||
import PaymentResultPage from "./PaymentResultPage";
|
||||
|
||||
class EntryPage extends React.Component {
|
||||
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="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} />
|
||||
<Route exact path="/select-plan/: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>
|
||||
</div>
|
||||
);
|
||||
|
@@ -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"),
|
||||
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"),
|
||||
dataIndex: "user",
|
||||
@@ -158,14 +158,6 @@ class PaymentListPage extends BaseListPage {
|
||||
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"),
|
||||
dataIndex: "type",
|
||||
@@ -187,6 +179,13 @@ class PaymentListPage extends BaseListPage {
|
||||
// width: '160px',
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("productDisplayName"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/products/${record.owner}/${record.productName}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("product:Price"),
|
||||
@@ -265,7 +264,7 @@ class PaymentListPage extends BaseListPage {
|
||||
value = params.type;
|
||||
}
|
||||
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) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
@@ -15,17 +15,24 @@
|
||||
import React from "react";
|
||||
import {Button, Result, Spin} from "antd";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
import * as PricingBackend from "./backend/PricingBackend";
|
||||
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class PaymentResultPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
this.state = {
|
||||
classes: props,
|
||||
paymentName: props.match.params.paymentName,
|
||||
organizationName: props.match.params.organizationName,
|
||||
owner: props.match?.params?.organizationName ?? props.match?.params?.owner ?? null,
|
||||
paymentName: props.match?.params?.paymentName ?? null,
|
||||
pricingName: props.pricingName ?? props.match?.params?.pricingName ?? null,
|
||||
subscriptionName: params.get("subscription"),
|
||||
payment: null,
|
||||
pricing: props.pricing ?? null,
|
||||
subscription: props.subscription ?? null,
|
||||
timeout: null,
|
||||
};
|
||||
}
|
||||
@@ -40,28 +47,77 @@ class PaymentResultPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getPayment() {
|
||||
PaymentBackend.getPayment(this.state.organizationName, this.state.paymentName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
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)});
|
||||
}
|
||||
}
|
||||
setStateAsync(state) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.setState(state, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -81,7 +137,7 @@ class PaymentResultPage extends React.Component {
|
||||
|
||||
if (payment.state === "Paid") {
|
||||
return (
|
||||
<div>
|
||||
<div className="login-content">
|
||||
{
|
||||
Setting.renderHelmet(payment)
|
||||
}
|
||||
@@ -101,7 +157,7 @@ class PaymentResultPage extends React.Component {
|
||||
);
|
||||
} else if (payment.state === "Created") {
|
||||
return (
|
||||
<div>
|
||||
<div className="login-content">
|
||||
{
|
||||
Setting.renderHelmet(payment)
|
||||
}
|
||||
@@ -117,7 +173,7 @@ class PaymentResultPage extends React.Component {
|
||||
);
|
||||
} else if (payment.state === "Canceled") {
|
||||
return (
|
||||
<div>
|
||||
<div className="login-content">
|
||||
{
|
||||
Setting.renderHelmet(payment)
|
||||
}
|
||||
@@ -137,7 +193,7 @@ class PaymentResultPage extends React.Component {
|
||||
);
|
||||
} else if (payment.state === "Timeout") {
|
||||
return (
|
||||
<div>
|
||||
<div className="login-content">
|
||||
{
|
||||
Setting.renderHelmet(payment)
|
||||
}
|
||||
@@ -157,7 +213,7 @@ class PaymentResultPage extends React.Component {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div className="login-content">
|
||||
{
|
||||
Setting.renderHelmet(payment)
|
||||
}
|
||||
|
@@ -110,11 +110,12 @@ class PermissionListPage extends BaseListPage {
|
||||
|
||||
return (
|
||||
<Upload {...props}>
|
||||
<Button type="primary" size="small">
|
||||
<Button id="upload-button" type="primary" size="small">
|
||||
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
||||
</Button></Upload>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable(permissions) {
|
||||
const columns = [
|
||||
// https://github.com/ant-design/ant-design/issues/22184
|
||||
@@ -361,7 +362,7 @@ class PermissionListPage extends BaseListPage {
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Permissions")}
|
||||
<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()
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as RoleBackend from "./backend/RoleBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
@@ -28,14 +29,14 @@ class PlanEditPage extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
planName: props.match.params.planName,
|
||||
organizationName: props?.organizationName ?? props?.match?.params?.organizationName ?? null,
|
||||
planName: props?.match?.params?.planName ?? null,
|
||||
plan: null,
|
||||
organizations: [],
|
||||
users: [],
|
||||
roles: [],
|
||||
providers: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
paymentProviders: [],
|
||||
mode: props?.location?.mode ?? "edit",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ class PlanEditPage extends React.Component {
|
||||
|
||||
this.getUsers(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() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
@@ -165,7 +181,7 @@ class PlanEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.role} onChange={(value => {this.updatePlanField("role", value);})}
|
||||
options={this.state.roles.map((role) => Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`))
|
||||
options={this.state.roles.map((role) => Setting.getOption(role.name, role.name))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -181,22 +197,27 @@ class PlanEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("plan:Price per month"), i18next.t("plan:Price per month - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("plan:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => {
|
||||
this.updatePlanField("pricePerMonth", value);
|
||||
<InputNumber value={this.state.plan.price} onChange={value => {
|
||||
this.updatePlanField("price", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<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 span={22} >
|
||||
<InputNumber value={this.state.plan.pricePerYear} onChange={value => {
|
||||
this.updatePlanField("pricePerYear", value);
|
||||
}} />
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.period} onChange={value => {
|
||||
this.updatePlanField("period", value);
|
||||
}}
|
||||
options={[
|
||||
{value: "Monthly", label: "Monthly"},
|
||||
{value: "Yearly", label: "Yearly"},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@@ -216,6 +237,18 @@ class PlanEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||
|
@@ -32,10 +32,11 @@ class PlanListPage extends BaseListPage {
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Plan - ${randomName}`,
|
||||
description: "",
|
||||
pricePerMonth: 10,
|
||||
pricePerYear: 100,
|
||||
price: 10,
|
||||
currency: "USD",
|
||||
period: "Monthly",
|
||||
isEnabled: true,
|
||||
paymentProviders: [],
|
||||
role: "",
|
||||
options: [],
|
||||
};
|
||||
@@ -127,18 +128,26 @@ class PlanListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price per month"),
|
||||
dataIndex: "pricePerMonth",
|
||||
key: "pricePerMonth",
|
||||
width: "130px",
|
||||
...this.getColumnSearchProps("pricePerMonth"),
|
||||
title: i18next.t("payment:Currency"),
|
||||
dataIndex: "currency",
|
||||
key: "currency",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("currency"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price per year"),
|
||||
dataIndex: "pricePerYear",
|
||||
key: "pricePerYear",
|
||||
title: i18next.t("plan:Price"),
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
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"),
|
||||
@@ -148,7 +157,21 @@ class PlanListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("role"),
|
||||
render: (text, record, index) => {
|
||||
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}
|
||||
</Link>
|
||||
);
|
||||
|
@@ -190,7 +190,7 @@ class PricingEditPage extends React.Component {
|
||||
onChange={(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>
|
||||
</Row>
|
||||
@@ -294,7 +294,7 @@ class PricingEditPage extends React.Component {
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
<PricingPage pricing={this.state.pricing}></PricingPage>
|
||||
<PricingPage pricing={this.state.pricing} owner={this.state.pricing.owner}></PricingPage>
|
||||
</Col>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
@@ -14,7 +14,8 @@
|
||||
|
||||
import React from "react";
|
||||
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 * as Setting from "./Setting";
|
||||
import * as PricingBackend from "./backend/PricingBackend";
|
||||
@@ -118,11 +119,58 @@ class PricingListPage extends BaseListPage {
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
// width: "170px",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...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"),
|
||||
dataIndex: "isEnabled",
|
||||
|
@@ -17,16 +17,24 @@ import {Button, Descriptions, Modal, Spin} from "antd";
|
||||
import {CheckCircleTwoTone} from "@ant-design/icons";
|
||||
import i18next from "i18next";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import * as PricingBackend from "./backend/PricingBackend";
|
||||
import * as Setting from "./Setting";
|
||||
|
||||
class ProductBuyPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props?.match?.params?.organizationName,
|
||||
productName: props.productName !== undefined ? props.productName : props?.match?.params?.productName,
|
||||
owner: props?.organizationName ?? props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
|
||||
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,
|
||||
pricing: props?.pricing ?? null,
|
||||
plan: null,
|
||||
isPlacingOrder: false,
|
||||
qrCodeModalProvider: null,
|
||||
};
|
||||
@@ -36,20 +44,57 @@ class ProductBuyPage extends React.Component {
|
||||
this.getProduct();
|
||||
}
|
||||
|
||||
getProduct() {
|
||||
if (this.state.productName === undefined || this.state.organizationName === undefined) {
|
||||
setStateAsync(state) {
|
||||
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 ;
|
||||
}
|
||||
ProductBackend.getProduct(this.state.organizationName, this.state.productName)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
try {
|
||||
// load pricing & plan
|
||||
if (this.state.pricingName) {
|
||||
if (!this.state.planName || !this.state.userName) {
|
||||
return ;
|
||||
}
|
||||
this.setState({
|
||||
product: res.data,
|
||||
let res = await PricingBackend.getPricing(this.state.owner, this.state.pricingName);
|
||||
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() {
|
||||
@@ -96,7 +141,7 @@ class ProductBuyPage extends React.Component {
|
||||
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) => {
|
||||
if (res.status === "ok") {
|
||||
const payUrl = res.data;
|
||||
@@ -215,11 +260,11 @@ class ProductBuyPage extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="login-content">
|
||||
<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}>
|
||||
<span style={{fontSize: 28}}>
|
||||
<span style={{fontSize: 25}}>
|
||||
{Setting.getLanguageText(product?.displayName)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
|
@@ -98,6 +98,7 @@ class ProductEditPage extends React.Component {
|
||||
}
|
||||
|
||||
renderProduct() {
|
||||
const isCreatedByPlan = this.state.product.tag === "auto_created_product_for_plan";
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<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}
|
||||
</div>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} 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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.product.name} onChange={e => {
|
||||
<Input value={this.state.product.name} disabled={isCreatedByPlan} onChange={e => {
|
||||
this.updateProductField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -127,18 +140,6 @@ class ProductEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{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"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -201,7 +202,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
})}>
|
||||
{
|
||||
@@ -218,7 +219,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -228,7 +229,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -238,7 +239,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -248,7 +249,7 @@ class ProductEditPage extends React.Component {
|
||||
{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.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>)
|
||||
}
|
||||
@@ -312,7 +313,7 @@ class ProductEditPage extends React.Component {
|
||||
|
||||
submitProductEdit(willExist) {
|
||||
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) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
|
@@ -253,11 +253,13 @@ class ProductListPage extends BaseListPage {
|
||||
width: "230px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
const isCreatedByPlan = record.tag === "auto_created_product_for_plan";
|
||||
return (
|
||||
<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"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
disabled={isCreatedByPlan}
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteProduct(index)}
|
||||
>
|
||||
|
@@ -423,13 +423,13 @@ class ProviderEditPage extends React.Component {
|
||||
[
|
||||
{id: "Captcha", name: "Captcha"},
|
||||
{id: "Email", name: "Email"},
|
||||
{id: "Notification", name: "Notification"},
|
||||
{id: "OAuth", name: "OAuth"},
|
||||
{id: "Payment", name: "Payment"},
|
||||
{id: "SAML", name: "SAML"},
|
||||
{id: "SMS", name: "SMS"},
|
||||
{id: "Storage", name: "Storage"},
|
||||
{id: "Web3", name: "Web3"},
|
||||
{id: "Notification", name: "Notification"},
|
||||
]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
|
||||
|
@@ -142,13 +142,15 @@ class ProviderListPage extends BaseListPage {
|
||||
key: "category",
|
||||
filterMultiple: false,
|
||||
filters: [
|
||||
{text: "OAuth", value: "OAuth"},
|
||||
{text: "Captcha", value: "Captcha"},
|
||||
{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: "Storage", value: "Storage"},
|
||||
{text: "SAML", value: "SAML"},
|
||||
{text: "Captcha", value: "Captcha"},
|
||||
{text: "Payment", value: "Payment"},
|
||||
{text: "Web3", value: "Web3"},
|
||||
],
|
||||
width: "110px",
|
||||
sorter: true,
|
||||
@@ -161,13 +163,15 @@ class ProviderListPage extends BaseListPage {
|
||||
align: "center",
|
||||
filterMultiple: false,
|
||||
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: "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: "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: "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};})},
|
||||
{text: "Web3", value: "Web3", children: Setting.getProviderTypeOptions("Web3").map((o) => {return {text: o.id, value: o.name};})},
|
||||
],
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
@@ -237,7 +241,7 @@ class ProviderListPage extends BaseListPage {
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Providers")}
|
||||
<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>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
|
@@ -76,7 +76,7 @@ class ResourceListPage extends BaseListPage {
|
||||
return (
|
||||
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false}
|
||||
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...")}
|
||||
</Button>
|
||||
</Upload>
|
||||
|
@@ -274,7 +274,7 @@ export const OtherProviderInfo = {
|
||||
},
|
||||
"Custom HTTP": {
|
||||
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: "AWS S3", name: "AWS S3"},
|
||||
{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: "Azure Blob", name: "Azure Blob"},
|
||||
{id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"},
|
||||
@@ -1170,9 +1170,9 @@ export function getTags(tags, urlPrefix = null) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getTag(color, text) {
|
||||
export function getTag(color, text, icon) {
|
||||
return (
|
||||
<Tag color={color}>
|
||||
<Tag color={color} icon={icon}>
|
||||
{text}
|
||||
</Tag>
|
||||
);
|
||||
@@ -1254,3 +1254,13 @@ export function builtInObject(obj) {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -14,8 +14,9 @@
|
||||
|
||||
import moment from "moment";
|
||||
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 PricingBackend from "./backend/PricingBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
@@ -33,7 +34,8 @@ class SubscriptionEditPage extends React.Component {
|
||||
subscription: null,
|
||||
organizations: [],
|
||||
users: [],
|
||||
planes: [],
|
||||
pricings: [],
|
||||
plans: [],
|
||||
providers: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
@@ -62,15 +64,25 @@ class SubscriptionEditPage extends React.Component {
|
||||
});
|
||||
|
||||
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)
|
||||
.then((res) => {
|
||||
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 => {
|
||||
this.updateSubscriptionField("owner", owner);
|
||||
this.getUsers(owner);
|
||||
this.getPlanes(owner);
|
||||
this.getPlans(owner);
|
||||
})}
|
||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
||||
} />
|
||||
@@ -161,32 +173,39 @@ class SubscriptionEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:Duration"), i18next.t("subscription:Duration - Tooltip"))}
|
||||
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.subscription.duration} onChange={value => {
|
||||
this.updateSubscriptionField("duration", value);
|
||||
<DatePicker value={dayjs(this.state.subscription.startTime)} onChange={value => {
|
||||
this.updateSubscriptionField("startTime", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:Start date"), i18next.t("subscription:Start date - Tooltip"))}
|
||||
{Setting.getLabel(i18next.t("subscription:End time"), i18next.t("subscription:End time - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<DatePicker value={dayjs(this.state.subscription.startDate)} onChange={value => {
|
||||
this.updateSubscriptionField("startDate", value);
|
||||
<DatePicker value={dayjs(this.state.subscription.endTime)} onChange={value => {
|
||||
this.updateSubscriptionField("endTime", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:End date"), i18next.t("subscription:End date - Tooltip"))}
|
||||
{Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<DatePicker value={dayjs(this.state.subscription.endDate)} onChange={value => {
|
||||
this.updateSubscriptionField("endDate", value);
|
||||
}} />
|
||||
<Select
|
||||
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>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@@ -196,21 +215,42 @@ class SubscriptionEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Select style={{width: "100%"}} value={this.state.subscription.user}
|
||||
onChange={(value => {this.updateSubscriptionField("user", value);})}
|
||||
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
|
||||
options={this.state.users.map((user) => Setting.getOption(user.name, user.name))}
|
||||
/>
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Plan"), i18next.t("general:Plan - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.plan} onChange={(value => {this.updateSubscriptionField("plan", value);})}
|
||||
options={this.state.planes.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`))
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.plan}
|
||||
onChange={(value => {this.updateSubscriptionField("plan", value);})}
|
||||
options={this.state.plans.map((plan) => Setting.getOption(plan.name, plan.name))
|
||||
} />
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
|
||||
@@ -221,46 +261,6 @@ class SubscriptionEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.subscription.isEnabled} onChange={checked => {
|
||||
this.updateSubscriptionField("isEnabled", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
|
||||
@@ -280,8 +280,12 @@ class SubscriptionEditPage extends React.Component {
|
||||
this.updateSubscriptionField("state", value);
|
||||
})}
|
||||
options={[
|
||||
{value: "Approved", name: i18next.t("permission:Approved")},
|
||||
{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))}
|
||||
/>
|
||||
</Col>
|
||||
|
@@ -15,6 +15,7 @@
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import {ClockCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, MinusCircleOutlined, SyncOutlined} from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||
@@ -26,24 +27,19 @@ class SubscriptionListPage extends BaseListPage {
|
||||
newSubscription() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
const defaultDuration = 365;
|
||||
|
||||
return {
|
||||
owner: owner,
|
||||
name: `subscription_${randomName}`,
|
||||
name: `sub_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Subscription - ${randomName}`,
|
||||
startDate: moment().format(),
|
||||
endDate: moment().add(defaultDuration, "d").format(),
|
||||
duration: defaultDuration,
|
||||
startTime: moment().format(),
|
||||
endTime: moment().add(30, "d").format(),
|
||||
period: "Monthly",
|
||||
description: "",
|
||||
user: "",
|
||||
plan: "",
|
||||
isEnabled: true,
|
||||
submitter: this.props.account.name,
|
||||
approver: this.props.account.name,
|
||||
approveTime: moment().format(),
|
||||
state: "Approved",
|
||||
state: "Active",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,11 +129,25 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Duration"),
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
title: i18next.t("subscription:Period"),
|
||||
dataIndex: "period",
|
||||
key: "period",
|
||||
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"),
|
||||
@@ -147,7 +157,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("plan"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/plans/${text}`}>
|
||||
<Link to={`/plans/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
@@ -161,7 +171,21 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("user"),
|
||||
render: (text, record, index) => {
|
||||
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}
|
||||
</Link>
|
||||
);
|
||||
@@ -176,10 +200,18 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("state"),
|
||||
render: (text, record, index) => {
|
||||
switch (text) {
|
||||
case "Approved":
|
||||
return Setting.getTag("success", i18next.t("permission:Approved"));
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
|
@@ -234,12 +234,60 @@ class SyncerEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@@ -274,24 +322,6 @@ class SyncerEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
|
@@ -86,8 +86,13 @@ class SyncerListPage extends BaseListPage {
|
||||
this.setState({loading: true});
|
||||
SyncerBackend.runSyncer("admin", this.state.data[i].name)
|
||||
.then((res) => {
|
||||
this.setState({loading: false});
|
||||
Setting.showMessage("success", "Syncer sync users successfully");
|
||||
if (res.status === "ok") {
|
||||
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 => {
|
||||
@@ -151,6 +156,13 @@ class SyncerListPage extends BaseListPage {
|
||||
{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"),
|
||||
dataIndex: "host",
|
||||
@@ -183,13 +195,6 @@ class SyncerListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
...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"),
|
||||
dataIndex: "database",
|
||||
@@ -208,7 +213,7 @@ class SyncerListPage extends BaseListPage {
|
||||
title: i18next.t("syncer:Sync interval"),
|
||||
dataIndex: "syncInterval",
|
||||
key: "syncInterval",
|
||||
width: "130px",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("syncInterval"),
|
||||
},
|
||||
|
@@ -12,10 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// 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 React from "react";
|
||||
import * as Setting from "./Setting";
|
||||
import * as TourConfig from "./TourConfig";
|
||||
import i18next from "i18next";
|
||||
import PrometheusInfoTable from "./table/PrometheusInfoTable";
|
||||
|
||||
@@ -29,6 +30,7 @@ class SystemInfo extends React.Component {
|
||||
prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
|
||||
intervalId: null,
|
||||
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() {
|
||||
if (this.state.intervalId !== null) {
|
||||
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() {
|
||||
const cpuUi = this.state.systemInfo.cpuUsage?.length <= 0 ? i18next.t("system:Failed to get CPU usage") :
|
||||
this.state.systemInfo.cpuUsage.map((usage, i) => {
|
||||
@@ -99,45 +137,58 @@ class SystemInfo extends React.Component {
|
||||
|
||||
if (!Setting.isMobile()) {
|
||||
return (
|
||||
<Row>
|
||||
<Col span={6}></Col>
|
||||
<Col span={12}>
|
||||
<Row gutter={[10, 10]}>
|
||||
<Col span={12}>
|
||||
<Card title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : cpuUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : memUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : latencyUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={i18next.t("system:API Throughput")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : throughputUi}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
|
||||
<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>
|
||||
GitHub: <a target="_blank" rel="noreferrer" href="https://github.com/casdoor/casdoor">Casdoor</a>
|
||||
<br />
|
||||
{i18next.t("system:Version")}: <a target="_blank" rel="noreferrer" href={link}>{versionText}</a>
|
||||
<br />
|
||||
{i18next.t("system:Official website")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org">https://casdoor.org</a>
|
||||
<br />
|
||||
{i18next.t("system:Community")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org/#:~:text=Casdoor%20API-,Community,-GitHub">Get in Touch!</a>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}></Col>
|
||||
</Row>
|
||||
<>
|
||||
<Row>
|
||||
<Col span={6}></Col>
|
||||
<Col span={12}>
|
||||
<Row gutter={[10, 10]}>
|
||||
<Col span={12}>
|
||||
<Card id="cpu-card" title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : cpuUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card id="memory-card" title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : memUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card id="latency-card" title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : latencyUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card id="throughput-card" title={i18next.t("system:API Throughput")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : throughputUi}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
<Card id="about-card" title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
|
||||
<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>
|
||||
GitHub: <a target="_blank" rel="noreferrer" href="https://github.com/casdoor/casdoor">Casdoor</a>
|
||||
<br />
|
||||
{i18next.t("system:Version")}: <a target="_blank" rel="noreferrer" href={link}>{versionText}</a>
|
||||
<br />
|
||||
{i18next.t("system:Official website")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org">https://casdoor.org</a>
|
||||
<br />
|
||||
{i18next.t("system:Community")}: <a target="_blank" rel="noreferrer" href="https://casdoor.org/#:~:text=Casdoor%20API-,Community,-GitHub">Get in Touch!</a>
|
||||
</Card>
|
||||
</Col>
|
||||
<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 {
|
||||
return (
|
||||
|
229
web/src/TourConfig.js
Normal file
229
web/src/TourConfig.js
Normal 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;
|
||||
}
|
||||
}
|
@@ -390,7 +390,7 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<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>
|
||||
</Row>
|
||||
|
@@ -187,7 +187,7 @@ class UserListPage extends BaseListPage {
|
||||
|
||||
return (
|
||||
<Upload {...props}>
|
||||
<Button type="primary" size="small">
|
||||
<Button id="upload-button" type="primary" size="small">
|
||||
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
||||
</Button>
|
||||
</Upload>
|
||||
@@ -426,7 +426,7 @@ class UserListPage extends BaseListPage {
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Users")}
|
||||
<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()
|
||||
}
|
||||
|
@@ -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() {
|
||||
const preview = Setting.deepCopy(previewTemplate);
|
||||
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 (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
);
|
||||
|
@@ -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 {
|
||||
callback(res);
|
||||
}
|
||||
|
@@ -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) {
|
||||
return "/result";
|
||||
} else {
|
||||
@@ -173,13 +177,12 @@ class SignupPage extends React.Component {
|
||||
const application = this.getApplicationObj();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
values["plan"] = params.get("plan");
|
||||
values["pricing"] = params.get("pricing");
|
||||
|
||||
values.plan = params.get("plan");
|
||||
values.pricing = params.get("pricing");
|
||||
AuthBackend.signup(values)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
if (Setting.hasPromptPage(application)) {
|
||||
if (Setting.hasPromptPage(application) && (!values.plan || !values.pricing)) {
|
||||
AuthBackend.getAccount("")
|
||||
.then((res) => {
|
||||
let account = null;
|
||||
@@ -188,13 +191,13 @@ class SignupPage extends React.Component {
|
||||
account.organization = res.data2;
|
||||
|
||||
this.onUpdateAccount(account);
|
||||
Setting.goToLinkSoft(this, this.getResultPath(application));
|
||||
Setting.goToLinkSoft(this, this.getResultPath(application, values));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Setting.goToLinkSoft(this, this.getResultPath(application));
|
||||
Setting.goToLinkSoft(this, this.getResultPath(application, values));
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
|
||||
@@ -506,6 +509,21 @@ class SignupPage extends React.Component {
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (signupItem.name === "Invitation code") {
|
||||
return (
|
||||
<Form.Item
|
||||
name="invitationCode"
|
||||
label={i18next.t("application:Invitation code")}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("signup:Please input your invitation code!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (signupItem.name === "Agreement") {
|
||||
return AgreementModal.renderAgreementFormItem(application, required, tailFormItemLayout, this);
|
||||
}
|
||||
|
@@ -14,8 +14,8 @@
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getPayments(owner, organization, 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}`, {
|
||||
export function getPayments(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
|
@@ -24,18 +24,8 @@ export function getPlans(owner, page = "", pageSize = "", field = "", value = ""
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getPlanById(id, includeOption = false) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${id}&includeOption=${includeOption}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getPlan(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
export function getPlan(owner, name, includeOption = false) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${owner}/${encodeURIComponent(name)}&includeOption=${includeOption}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
|
@@ -70,8 +70,8 @@ export function deleteProduct(product) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function buyProduct(owner, name, providerId) {
|
||||
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerId}`, {
|
||||
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
|
@@ -37,7 +37,7 @@ const AppListPage = (props) => {
|
||||
return applications.map(application => {
|
||||
let homepageUrl = application.homepageUrl;
|
||||
if (homepageUrl === "<custom-url>") {
|
||||
homepageUrl = this.props.account.homepage;
|
||||
homepageUrl = props.account.homepage;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -13,15 +13,23 @@
|
||||
// limitations under the License.
|
||||
|
||||
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 i18next from "i18next";
|
||||
import React from "react";
|
||||
import * as DashboardBackend from "../backend/DashboardBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import * as TourConfig from "../TourConfig";
|
||||
|
||||
const Dashboard = (props) => {
|
||||
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(() => {
|
||||
if (!Setting.isLocalAdminUser(props.account)) {
|
||||
@@ -42,6 +50,35 @@ const Dashboard = (props) => {
|
||||
});
|
||||
}, [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 = () => {
|
||||
if (dashboardData === null) {
|
||||
return;
|
||||
@@ -83,7 +120,7 @@ const Dashboard = (props) => {
|
||||
myChart.setOption(option);
|
||||
|
||||
return (
|
||||
<Row gutter={80}>
|
||||
<Row id="statistic" gutter={80}>
|
||||
<Col span={50}>
|
||||
<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"}} />
|
||||
@@ -112,6 +149,17 @@ const Dashboard = (props) => {
|
||||
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
|
||||
{renderEChart()}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
50
web/src/common/OpenTour.js
Normal file
50
web/src/common/OpenTour.js
Normal 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;
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Left",
|
||||
"Logged in successfully": "Logged in successfully",
|
||||
"Logged out successfully": "Logged out successfully",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Select a category",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse",
|
||||
"Parse metadata successfully": "Parse metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Is triggered"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copy Link",
|
||||
"File name": "File name",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Please input your affiliation!",
|
||||
"Please input your display name!": "Please input your display name!",
|
||||
"Please input your first name!": "Please input your first name!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Please input your last name!",
|
||||
"Please input your phone number!": "Please input your phone number!",
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"Is admin": "Is admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Wählen Sie aus, welche Grant-Typen im OAuth-Protokoll zulässig sind",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Links",
|
||||
"Logged in successfully": "Erfolgreich eingeloggt",
|
||||
"Logged out successfully": "Erfolgreich ausgeloggt",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "Das Public-Key-Zertifikat, das vom Client-SDK, das mit dieser Anwendung korrespondiert, verifiziert werden muss",
|
||||
"Certs": "Zertifikate",
|
||||
"Click to Upload": "Klicken Sie zum Hochladen",
|
||||
"Client IP": "Client-IP",
|
||||
"Close": "Schließen",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Erstellte Zeit",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Provider, die konfiguriert werden müssen, einschließlich Drittanbieter-Logins, Objektspeicherung, Verifizierungscode usw.",
|
||||
"Real name": "Echter Name",
|
||||
"Records": "Datensätze",
|
||||
"Request URI": "Anforderungs-URI",
|
||||
"Resources": "Ressourcen",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "Systeminformationen",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Dies ist eine schreibgeschützte Demo-Seite!",
|
||||
"Timestamp": "Zeitstempel",
|
||||
"Tokens": "Token",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agenten-ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App-ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App-Key",
|
||||
"App key - Tooltip": "App-Schlüssel",
|
||||
"App secret": "App-Secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Wählen Sie eine Kategorie aus",
|
||||
"Channel No.": "Kanal Nr.",
|
||||
"Channel No. - Tooltip": "Kanalnummer.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client-ID",
|
||||
"Client ID - Tooltip": "Client-ID",
|
||||
"Client ID 2": "Client-ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Anmeldeverfahren, QR-Code oder Silent-Login",
|
||||
"New Provider": "Neuer Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "parsen",
|
||||
"Parse metadata successfully": "Metadaten erfolgreich analysiert",
|
||||
"Path prefix": "Pfadpräfix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Ist ausgelöst"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Link kopieren",
|
||||
"File name": "Dateiname",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Bitte geben Sie Ihre Zugehörigkeit ein!",
|
||||
"Please input your display name!": "Bitte geben Sie Ihren Anzeigenamen ein!",
|
||||
"Please input your first name!": "Bitte geben Sie Ihren Vornamen ein!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Bitte geben Sie Ihren Nachnamen ein!",
|
||||
"Please input your phone number!": "Bitte geben Sie Ihre Telefonnummer ein!",
|
||||
"Please input your real name!": "Bitte geben Sie Ihren richtigen Namen ein!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Geben Sie Ihre E-Mail-Adresse ein",
|
||||
"Input your phone number": "Geben Sie Ihre Telefonnummer ein",
|
||||
"Is admin": "Ist Admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Left",
|
||||
"Logged in successfully": "Logged in successfully",
|
||||
"Logged out successfully": "Logged out successfully",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Select a category",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse",
|
||||
"Parse metadata successfully": "Parse metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Is triggered"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copy Link",
|
||||
"File name": "File name",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Please input your affiliation!",
|
||||
"Please input your display name!": "Please input your display name!",
|
||||
"Please input your first name!": "Please input your first name!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Please input your last name!",
|
||||
"Please input your phone number!": "Please input your phone number!",
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"Is admin": "Is admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Selecciona cuáles tipos de subvenciones están permitidas en el protocolo OAuth",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Izquierda",
|
||||
"Logged in successfully": "Acceso satisfactorio",
|
||||
"Logged out successfully": "Cerró sesión exitosamente",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "El certificado de clave pública que necesita ser verificado por el SDK del cliente correspondiente a esta aplicación",
|
||||
"Certs": "Certificaciones",
|
||||
"Click to Upload": "Haz clic para cargar",
|
||||
"Client IP": "Dirección IP del cliente",
|
||||
"Close": "Cerca",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Tiempo creado",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Proveedores a configurar, incluyendo inicio de sesión de terceros, almacenamiento de objetos, código de verificación, etc.",
|
||||
"Real name": "Nombre real",
|
||||
"Records": "Registros",
|
||||
"Request URI": "URI de solicitud",
|
||||
"Resources": "Recursos",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "Información del Sistema",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "¡Este es un sitio de demostración solo de lectura!",
|
||||
"Timestamp": "Marca de tiempo",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Identificador de agente",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "ID de aplicación",
|
||||
"App ID - Tooltip": "Identificador de la aplicación",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "Clave de aplicación",
|
||||
"App key - Tooltip": "Clave de aplicación",
|
||||
"App secret": "Secreto de la aplicación",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Selecciona una categoría",
|
||||
"Channel No.": "Canal No.",
|
||||
"Channel No. - Tooltip": "Canal No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Identificación de cliente",
|
||||
"Client ID - Tooltip": "Identificación del cliente",
|
||||
"Client ID 2": "Identificación de cliente 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Método de inicio de sesión, código QR o inicio de sesión silencioso",
|
||||
"New Provider": "Nuevo proveedor",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Analizar",
|
||||
"Parse metadata successfully": "Analizar los metadatos con éxito",
|
||||
"Path prefix": "Prefijo de ruta",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "administrador (compartido)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Es desencadenado / es disparado / es activado"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copiar enlace",
|
||||
"File name": "Nombre del archivo",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "¡Por favor, ingrese su afiliación!",
|
||||
"Please input your display name!": "¡Por favor ingrese su nombre de pantalla!",
|
||||
"Please input your first name!": "¡Por favor ingrese su primer nombre!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "¡Por favor ingrese su apellido!",
|
||||
"Please input your phone number!": "¡Por favor, ingrese su número de teléfono!",
|
||||
"Please input your real name!": "¡Por favor, ingresa tu nombre real!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Introduce tu correo electrónico",
|
||||
"Input your phone number": "Ingrese su número de teléfono",
|
||||
"Is admin": "Es el administrador",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Left",
|
||||
"Logged in successfully": "Logged in successfully",
|
||||
"Logged out successfully": "Logged out successfully",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Select a category",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse",
|
||||
"Parse metadata successfully": "Parse metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Is triggered"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copy Link",
|
||||
"File name": "File name",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Please input your affiliation!",
|
||||
"Please input your display name!": "Please input your display name!",
|
||||
"Please input your first name!": "Please input your first name!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Please input your last name!",
|
||||
"Please input your phone number!": "Please input your phone number!",
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"Is admin": "Is admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Sélectionnez les types d'autorisations autorisés dans le protocole OAuth",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "gauche",
|
||||
"Logged in successfully": "Connecté avec succès",
|
||||
"Logged out successfully": "Déconnecté avec succès",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "Le certificat de clé publique qui doit être vérifié par le kit de développement client correspondant à cette application",
|
||||
"Certs": "Certains",
|
||||
"Click to Upload": "Cliquez pour télécharger",
|
||||
"Client IP": "Adresse IP du client",
|
||||
"Close": "Fermer",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Temps créé",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Les fournisseurs doivent être configurés, y compris la connexion de tiers, le stockage d'objets, le code de vérification, etc.",
|
||||
"Real name": "Nom réel",
|
||||
"Records": "Dossiers",
|
||||
"Request URI": "URI de demande",
|
||||
"Resources": "Ressources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "Information système",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Ceci est un site de démonstration en lecture seule !",
|
||||
"Timestamp": "Horodatage",
|
||||
"Tokens": "Les jetons",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Identifiant d'agent",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "Identifiant d'application",
|
||||
"App ID - Tooltip": "Identifiant d'application",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "Clé d'application",
|
||||
"App key - Tooltip": "Clé d'application",
|
||||
"App secret": "Secret d'application",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Sélectionnez une catégorie",
|
||||
"Channel No.": "chaîne n°",
|
||||
"Channel No. - Tooltip": "Canal N°",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Identifiant client",
|
||||
"Client ID - Tooltip": "Identifiant du client",
|
||||
"Client ID 2": "Identifiant client 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Méthode de connexion, code QR ou connexion silencieuse",
|
||||
"New Provider": "Nouveau fournisseur",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parser",
|
||||
"Parse metadata successfully": "Parcourir les métadonnées avec succès",
|
||||
"Path prefix": "Préfixe de chemin",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Partagé)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Est déclenché"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copier le lien",
|
||||
"File name": "Nom de fichier",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Veuillez indiquer votre affiliation !",
|
||||
"Please input your display name!": "S'il vous plaît entrer votre nom d'affichage !",
|
||||
"Please input your first name!": "Veuillez entrer votre prénom !",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Veuillez entrer votre nom de famille !",
|
||||
"Please input your phone number!": "Veuillez saisir votre numéro de téléphone !",
|
||||
"Please input your real name!": "Veuillez entrer votre vrai nom!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Entrez votre adresse e-mail",
|
||||
"Input your phone number": "Saisissez votre numéro de téléphone",
|
||||
"Is admin": "Est l'administrateur",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Left",
|
||||
"Logged in successfully": "Logged in successfully",
|
||||
"Logged out successfully": "Logged out successfully",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Select a category",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse",
|
||||
"Parse metadata successfully": "Parse metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Is triggered"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copy Link",
|
||||
"File name": "File name",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Please input your affiliation!",
|
||||
"Please input your display name!": "Please input your display name!",
|
||||
"Please input your first name!": "Please input your first name!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Please input your last name!",
|
||||
"Please input your phone number!": "Please input your phone number!",
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"Is admin": "Is admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Pilih jenis hibah apa yang diperbolehkan dalam protokol OAuth",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Kiri",
|
||||
"Logged in successfully": "Berhasil masuk",
|
||||
"Logged out successfully": "Berhasil keluar dari sistem",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "Sertifikat kunci publik yang perlu diverifikasi oleh SDK klien yang sesuai dengan aplikasi ini",
|
||||
"Certs": "Sertifikat",
|
||||
"Click to Upload": "Klik untuk Mengunggah",
|
||||
"Client IP": "IP klien",
|
||||
"Close": "Tutup",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Waktu dibuat",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Penyedia harus dikonfigurasi, termasuk login pihak ketiga, penyimpanan objek, kode verifikasi, dan lain-lain.",
|
||||
"Real name": "Nama asli",
|
||||
"Records": "Catatan",
|
||||
"Request URI": "Permintaan URI",
|
||||
"Resources": "Sumber daya",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "Informasi Sistem",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Ini adalah situs demo hanya untuk dibaca saja!",
|
||||
"Timestamp": "Waktu penanda waktu",
|
||||
"Tokens": "Token-token",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "ID Agen",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "ID Aplikasi",
|
||||
"App ID - Tooltip": "ID Aplikasi",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "Kunci aplikasi",
|
||||
"App key - Tooltip": "Kunci aplikasi",
|
||||
"App secret": "Rahasia aplikasi",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Pilih kategori",
|
||||
"Channel No.": "Saluran nomor.",
|
||||
"Channel No. - Tooltip": "Saluran No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "ID klien",
|
||||
"Client ID - Tooltip": "ID klien",
|
||||
"Client ID 2": "ID klien 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Metode login, kode QR atau login tanpa suara",
|
||||
"New Provider": "Penyedia Baru",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse: Memecah atau mengurai data atau teks menjadi bagian-bagian yang lebih kecil dan lebih mudah dipahami atau dimanipulasi",
|
||||
"Parse metadata successfully": "Berhasil mem-parse metadata",
|
||||
"Path prefix": "Awalan jalur",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "Admin (Berbagi)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Ditimbulkan"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Salin Tautan",
|
||||
"File name": "Nama file",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Silakan masukkan afiliasi Anda!",
|
||||
"Please input your display name!": "Silakan masukkan nama tampilan Anda!",
|
||||
"Please input your first name!": "Silahkan masukkan nama depan Anda!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Silahkan masukkan nama belakang Anda!",
|
||||
"Please input your phone number!": "Silakan masukkan nomor telepon Anda!",
|
||||
"Please input your real name!": "Silakan masukkan nama asli Anda!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Masukkan alamat email Anda",
|
||||
"Input your phone number": "Masukkan nomor telepon Anda",
|
||||
"Is admin": "Apakah admin?",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Left",
|
||||
"Logged in successfully": "Logged in successfully",
|
||||
"Logged out successfully": "Logged out successfully",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Select a category",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse",
|
||||
"Parse metadata successfully": "Parse metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Is triggered"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copy Link",
|
||||
"File name": "File name",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Please input your affiliation!",
|
||||
"Please input your display name!": "Please input your display name!",
|
||||
"Please input your first name!": "Please input your first name!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Please input your last name!",
|
||||
"Please input your phone number!": "Please input your phone number!",
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"Is admin": "Is admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "OAuthプロトコルで許可されているグラントタイプを選択してください",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "左",
|
||||
"Logged in successfully": "正常にログインしました",
|
||||
"Logged out successfully": "正常にログアウトしました",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "このアプリケーションに対応するクライアントSDKによって検証する必要がある公開鍵証明書",
|
||||
"Certs": "証明書",
|
||||
"Click to Upload": "アップロードするにはクリックしてください",
|
||||
"Client IP": "クライアントIP",
|
||||
"Close": "閉じる",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "作成された時間",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "設定するプロバイダーには、サードパーティのログイン、オブジェクトストレージ、検証コードなどが含まれます。",
|
||||
"Real name": "本名",
|
||||
"Records": "記録",
|
||||
"Request URI": "リクエストURI",
|
||||
"Resources": "リソース",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "システム情報",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "これは読み取り専用のデモサイトです!",
|
||||
"Timestamp": "タイムスタンプ",
|
||||
"Tokens": "トークン",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "エージェントID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "アプリID",
|
||||
"App ID - Tooltip": "アプリID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "アプリキー",
|
||||
"App key - Tooltip": "アプリキー",
|
||||
"App secret": "アプリの秘密鍵",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "カテゴリーを選択してください",
|
||||
"Channel No.": "チャンネル番号",
|
||||
"Channel No. - Tooltip": "チャンネル番号",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "クライアントID",
|
||||
"Client ID - Tooltip": "クライアントID",
|
||||
"Client ID 2": "クライアントID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "ログイン方法、QRコードまたはサイレントログイン",
|
||||
"New Provider": "新しい提供者",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "パースする",
|
||||
"Parse metadata successfully": "メタデータを正常に解析しました",
|
||||
"Path prefix": "パスプレフィックス",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "管理者(共有)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "トリガーされています"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "コピー リンク",
|
||||
"File name": "ファイル名",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "所属を入力してください!",
|
||||
"Please input your display name!": "表示名を入力してください!",
|
||||
"Please input your first name!": "最初の名前を入力してください!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "あなたの姓を入力してください!",
|
||||
"Please input your phone number!": "あなたの電話番号を入力してください!",
|
||||
"Please input your real name!": "正しい名前を入力してください!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "あなたのメールアドレスを入力してください",
|
||||
"Input your phone number": "電話番号を入力してください",
|
||||
"Is admin": "管理者ですか?",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "OAuth 프로토콜에서 허용되는 그란트 유형을 선택하십시오",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "왼쪽",
|
||||
"Logged in successfully": "성공적으로 로그인했습니다",
|
||||
"Logged out successfully": "로그아웃이 성공적으로 되었습니다",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "이 응용 프로그램에 해당하는 클라이언트 SDK에서 확인해야 하는 공개 키 인증서",
|
||||
"Certs": "증명서",
|
||||
"Click to Upload": "클릭하여 업로드하세요",
|
||||
"Client IP": "고객 IP",
|
||||
"Close": "닫다",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "작성한 시간",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "공급 업체는 구성되어야합니다. 3rd-party 로그인, 객체 저장소, 검증 코드 등을 포함합니다.",
|
||||
"Real name": "실명",
|
||||
"Records": "기록",
|
||||
"Request URI": "요청 URI",
|
||||
"Resources": "자원",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "시스템 정보",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "이것은 읽기 전용 데모 사이트입니다!",
|
||||
"Timestamp": "타임스탬프",
|
||||
"Tokens": "토큰",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "에이전트 ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "앱 ID",
|
||||
"App ID - Tooltip": "앱 식별자",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "앱 키",
|
||||
"App key - Tooltip": "앱 키",
|
||||
"App secret": "앱 비밀키",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "카테고리를 선택하세요",
|
||||
"Channel No.": "채널 번호",
|
||||
"Channel No. - Tooltip": "채널 번호",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "클라이언트 ID",
|
||||
"Client ID - Tooltip": "클라이언트 ID",
|
||||
"Client ID 2": "고객 ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "로그인 방법, QR 코드 또는 음성 로그인",
|
||||
"New Provider": "새로운 공급 업체",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "파싱",
|
||||
"Parse metadata successfully": "메타데이터를 성공적으로 분석했습니다",
|
||||
"Path prefix": "경로 접두어",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "관리자 (공유)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "반응하다"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "링크 복사하기",
|
||||
"File name": "파일 이름",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "소속을 입력해주세요!",
|
||||
"Please input your display name!": "디스플레이 이름을 입력해주세요!",
|
||||
"Please input your first name!": "이름을 입력해주세요!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "성을 입력해주세요!",
|
||||
"Please input your phone number!": "전화번호를 입력해주세요!",
|
||||
"Please input your real name!": "진짜 이름을 입력해주세요!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "이메일을 입력하세요",
|
||||
"Input your phone number": "전화번호를 입력하세요",
|
||||
"Is admin": "어드민인가요?",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Left",
|
||||
"Logged in successfully": "Logged in successfully",
|
||||
"Logged out successfully": "Logged out successfully",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Select a category",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse",
|
||||
"Parse metadata successfully": "Parse metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Is triggered"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copy Link",
|
||||
"File name": "File name",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Please input your affiliation!",
|
||||
"Please input your display name!": "Please input your display name!",
|
||||
"Please input your first name!": "Please input your first name!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Please input your last name!",
|
||||
"Please input your phone number!": "Please input your phone number!",
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"Is admin": "Is admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Selecione quais tipos de concessão são permitidos no protocolo OAuth",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Esquerda",
|
||||
"Logged in successfully": "Login realizado com sucesso",
|
||||
"Logged out successfully": "Logout realizado com sucesso",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "O certificado da chave pública que precisa ser verificado pelo SDK do cliente correspondente a esta aplicação",
|
||||
"Certs": "Certificados",
|
||||
"Click to Upload": "Clique para Enviar",
|
||||
"Client IP": "IP do Cliente",
|
||||
"Close": "Fechar",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Hora de Criação",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Provedores a serem configurados, incluindo login de terceiros, armazenamento de objetos, código de verificação, etc.",
|
||||
"Real name": "Nome real",
|
||||
"Records": "Registros",
|
||||
"Request URI": "URI da solicitação",
|
||||
"Resources": "Recursos",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "Informações do Sistema",
|
||||
"There was a problem signing you in..": "Ocorreu um problema ao fazer o login.",
|
||||
"This is a read-only demo site!": "Este é um site de demonstração apenas para leitura!",
|
||||
"Timestamp": "Carimbo de Data/Hora",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "ID do Agente",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "ID do aplicativo",
|
||||
"App ID - Tooltip": "ID do aplicativo",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "Chave do aplicativo",
|
||||
"App key - Tooltip": "Chave do aplicativo",
|
||||
"App secret": "Segredo do aplicativo",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Selecione uma categoria",
|
||||
"Channel No.": "Número do canal",
|
||||
"Channel No. - Tooltip": "Número do canal",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "ID do cliente",
|
||||
"Client ID - Tooltip": "ID do cliente",
|
||||
"Client ID 2": "ID do cliente 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Método de login, código QR ou login silencioso",
|
||||
"New Provider": "Novo Provedor",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Analisar",
|
||||
"Parse metadata successfully": "Metadados analisados com sucesso",
|
||||
"Path prefix": "Prefixo do caminho",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Compartilhado)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Foi acionado"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copiar Link",
|
||||
"File name": "Nome do arquivo",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Por favor, insira sua afiliação!",
|
||||
"Please input your display name!": "Por favor, insira seu nome de exibição!",
|
||||
"Please input your first name!": "Por favor, insira seu primeiro nome!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Por favor, insira seu sobrenome!",
|
||||
"Please input your phone number!": "Por favor, insira seu número de telefone!",
|
||||
"Please input your real name!": "Por favor, insira seu nome real!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "Tipo de cartão de identidade",
|
||||
"ID card type - Tooltip": "Tipo de cartão de identidade - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Digite seu e-mail",
|
||||
"Input your phone number": "Digite seu número de telefone",
|
||||
"Is admin": "É administrador",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Выберите, какие типы грантов разрешены в протоколе OAuth",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Левый",
|
||||
"Logged in successfully": "Успешный вход в систему",
|
||||
"Logged out successfully": "Успешный выход из системы",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "Сертификат открытого ключа, который требуется проверить клиентским SDK, соответствующим этому приложению",
|
||||
"Certs": "сертификаты",
|
||||
"Click to Upload": "Нажмите, чтобы загрузить",
|
||||
"Client IP": "Клиентский IP",
|
||||
"Close": "Близко",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Созданное время",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Провайдеры должны быть настроены, включая вход с помощью сторонних сервисов, объектное хранилище, код подтверждения и т.д.",
|
||||
"Real name": "Реальное имя",
|
||||
"Records": "Записи",
|
||||
"Request URI": "Запрос URI",
|
||||
"Resources": "Ресурсы",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "Системная информация",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Это демонстрационный сайт только для чтения!",
|
||||
"Timestamp": "Отметка времени",
|
||||
"Tokens": "Токены",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Агент ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "Идентификатор приложения",
|
||||
"App ID - Tooltip": "Идентификатор приложения",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "Ключ приложения",
|
||||
"App key - Tooltip": "Ключ приложения",
|
||||
"App secret": "Секрет приложения",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Выберите категорию",
|
||||
"Channel No.": "Канал №",
|
||||
"Channel No. - Tooltip": "Номер канала.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Идентификатор клиента",
|
||||
"Client ID - Tooltip": "Идентификатор клиента",
|
||||
"Client ID 2": "Идентификатор клиента 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Метод входа, QR-код или беззвучный вход",
|
||||
"New Provider": "Новый провайдер",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Спарсить",
|
||||
"Parse metadata successfully": "Успешно обработана метаданные",
|
||||
"Path prefix": "Префикс пути",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "администратор (общий)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Сработало"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Копировать ссылку",
|
||||
"File name": "Имя файла",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Пожалуйста, укажите свою принадлежность!",
|
||||
"Please input your display name!": "Пожалуйста, введите своё отображаемое имя!",
|
||||
"Please input your first name!": "Пожалуйста, введите свое имя!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Введите свою фамилию!",
|
||||
"Please input your phone number!": "Пожалуйста, введите свой номер телефона!",
|
||||
"Please input your real name!": "Пожалуйста, введите своё настоящее имя!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Введите свой адрес электронной почты",
|
||||
"Input your phone number": "Введите ваш номер телефона",
|
||||
"Is admin": "Это администратор",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Left",
|
||||
"Logged in successfully": "Logged in successfully",
|
||||
"Logged out successfully": "Logged out successfully",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
|
||||
"Certs": "Certs",
|
||||
"Click to Upload": "Click to Upload",
|
||||
"Client IP": "Client IP",
|
||||
"Close": "Close",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "System Info",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Select a category",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Parse",
|
||||
"Parse metadata successfully": "Parse metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin (Shared)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Is triggered"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Copy Link",
|
||||
"File name": "File name",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Please input your affiliation!",
|
||||
"Please input your display name!": "Please input your display name!",
|
||||
"Please input your first name!": "Please input your first name!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Please input your last name!",
|
||||
"Please input your phone number!": "Please input your phone number!",
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"Is admin": "Is admin",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "Chọn loại hỗ trợ được cho phép trong giao thức OAuth",
|
||||
"Incremental": "Incremental",
|
||||
"Input": "Input",
|
||||
"Invitation code": "Invitation code",
|
||||
"Invitation code - Tooltip": "Invitation code - Tooltip",
|
||||
"Invitation code copied to clipboard successfully": "Invitation code copied to clipboard successfully",
|
||||
"Left": "Trái",
|
||||
"Logged in successfully": "Đăng nhập thành công",
|
||||
"Logged out successfully": "Đã đăng xuất thành công",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "Chứng chỉ khóa công khai cần được xác minh bởi SDK khách hàng tương ứng với ứng dụng này",
|
||||
"Certs": "Chứng chỉ",
|
||||
"Click to Upload": "Nhấp để tải lên",
|
||||
"Client IP": "Địa chỉ IP của khách hàng",
|
||||
"Close": "Đóng lại",
|
||||
"Confirm": "Confirm",
|
||||
"Copy": "Copy",
|
||||
"Created time": "Thời gian tạo",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "Các nhà cung cấp phải được cấu hình, bao gồm đăng nhập bên thứ ba, lưu trữ đối tượng, mã xác minh, v.v.",
|
||||
"Real name": "Tên thật",
|
||||
"Records": "Hồ sơ",
|
||||
"Request URI": "Yêu cầu URI",
|
||||
"Resources": "Tài nguyên",
|
||||
"Role": "Role",
|
||||
"Role - Tooltip": "Role - Tooltip",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "Thông tin hệ thống",
|
||||
"There was a problem signing you in..": "There was a problem signing you in..",
|
||||
"This is a read-only demo site!": "Đây là trang web giới thiệu chỉ có chức năng đọc!",
|
||||
"Timestamp": "Đánh dấu thời gian",
|
||||
"Tokens": "Mã thông báo",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Mã đại lý",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "ID ứng dụng",
|
||||
"App ID - Tooltip": "Định danh ứng dụng",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "Khóa ứng dụng",
|
||||
"App key - Tooltip": "Khóa ứng dụng",
|
||||
"App secret": "Mã bí mật ứng dụng",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "Chọn một danh mục",
|
||||
"Channel No.": "Kênh số.",
|
||||
"Channel No. - Tooltip": "Kênh Số.",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Mã khách hàng",
|
||||
"Client ID - Tooltip": "Mã khách hàng",
|
||||
"Client ID 2": "ID khách hàng 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "Phương thức đăng nhập, mã QR hoặc đăng nhập im lặng",
|
||||
"New Provider": "Nhà cung cấp mới",
|
||||
"Normal": "Normal",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "Phân tích cú pháp",
|
||||
"Parse metadata successfully": "Phân tích siêu dữ liệu thành công",
|
||||
"Path prefix": "Tiền tố đường dẫn",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "quản trị viên (Chung)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "Bị kích hoạt"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "Sao chép liên kết",
|
||||
"File name": "Tên tập tin",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "Vui lòng nhập thông tin về đơn vị của bạn!",
|
||||
"Please input your display name!": "Vui lòng nhập tên hiển thị của bạn!",
|
||||
"Please input your first name!": "Vui lòng nhập tên của bạn!",
|
||||
"Please input your invitation code!": "Please input your invitation code!",
|
||||
"Please input your last name!": "Vui lòng nhập họ của bạn!",
|
||||
"Please input your phone number!": "Vui lòng nhập số điện thoại của bạn!",
|
||||
"Please input your real name!": "Vui lòng nhập tên thật của bạn!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "ID card type",
|
||||
"ID card type - Tooltip": "ID card type - Tooltip",
|
||||
"ID card with person": "ID card with person",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "Nhập địa chỉ email của bạn",
|
||||
"Input your phone number": "Nhập số điện thoại của bạn",
|
||||
"Is admin": "Là quản trị viên",
|
||||
|
@@ -58,6 +58,9 @@
|
||||
"Grant types - Tooltip": "选择允许哪些OAuth协议中的grant types",
|
||||
"Incremental": "递增",
|
||||
"Input": "输入",
|
||||
"Invitation code": "邀请码",
|
||||
"Invitation code - Tooltip": "注册时填写的邀请码",
|
||||
"Invitation code copied to clipboard successfully": "邀请码成功复制到剪贴板",
|
||||
"Left": "居左",
|
||||
"Logged in successfully": "登录成功",
|
||||
"Logged out successfully": "登出成功",
|
||||
@@ -186,9 +189,9 @@
|
||||
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
|
||||
"Certs": "证书",
|
||||
"Click to Upload": "点击上传",
|
||||
"Client IP": "客户端IP",
|
||||
"Close": "关闭",
|
||||
"Confirm": "确认",
|
||||
"Copy": "复制",
|
||||
"Created time": "创建时间",
|
||||
"Custom": "自定义",
|
||||
"Dashboard": "数据看板",
|
||||
@@ -288,7 +291,6 @@
|
||||
"Providers - Tooltip": "需要配置的提供商,包括第三方登录、对象存储、验证码等",
|
||||
"Real name": "姓名",
|
||||
"Records": "日志",
|
||||
"Request URI": "请求URI",
|
||||
"Resources": "资源",
|
||||
"Role": "角色",
|
||||
"Role - Tooltip": "所对应的角色",
|
||||
@@ -327,7 +329,6 @@
|
||||
"System Info": "系统信息",
|
||||
"There was a problem signing you in..": "登录时遇到问题..",
|
||||
"This is a read-only demo site!": "这是一个只读演示站点!",
|
||||
"Timestamp": "时间戳",
|
||||
"Tokens": "令牌",
|
||||
"Type": "类型",
|
||||
"Type - Tooltip": "类型",
|
||||
@@ -634,10 +635,10 @@
|
||||
"Agent ID - Tooltip": "Agent ID",
|
||||
"Api Key": "Api Key",
|
||||
"Api Key - Tooltip": "Api Key - Tooltip",
|
||||
"Api Token": "Api Token",
|
||||
"Api Token - Tooltip": "Api Token - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID",
|
||||
"App Key": "App Key",
|
||||
"App Key - Tooltip": "App Key - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key",
|
||||
"App secret": "App secret",
|
||||
@@ -658,8 +659,8 @@
|
||||
"Category - Tooltip": "分类",
|
||||
"Channel No.": "Channel号码",
|
||||
"Channel No. - Tooltip": "Channel号码",
|
||||
"Chat Id": "Chat Id",
|
||||
"Chat Id - Tooltip": "Chat Id - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@@ -705,10 +706,8 @@
|
||||
"Method - Tooltip": "登录方法,二维码或者静默授权登录",
|
||||
"New Provider": "添加提供商",
|
||||
"Normal": "标准",
|
||||
"Notification content": "Notification content",
|
||||
"Notification content - Tooltip": "Notification content - Tooltip",
|
||||
"Parameter name": "Parameter name",
|
||||
"Parameter name - Tooltip": "Parameter name - Tooltip",
|
||||
"Parameter": "Parameter",
|
||||
"Parameter - Tooltip": "Parameter - Tooltip",
|
||||
"Parse": "解析",
|
||||
"Parse metadata successfully": "解析元数据成功",
|
||||
"Path prefix": "路径前缀",
|
||||
@@ -789,9 +788,6 @@
|
||||
"Wallets - Tooltip": "Wallets - Tooltip",
|
||||
"admin (Shared)": "admin(共享)"
|
||||
},
|
||||
"record": {
|
||||
"Is triggered": "已触发"
|
||||
},
|
||||
"resource": {
|
||||
"Copy Link": "复制链接",
|
||||
"File name": "文件名",
|
||||
@@ -826,6 +822,7 @@
|
||||
"Please input your affiliation!": "请输入您所在的工作单位!",
|
||||
"Please input your display name!": "请输入您的显示名称!",
|
||||
"Please input your first name!": "请输入您的名字!",
|
||||
"Please input your invitation code!": "请输入你的邀请码!",
|
||||
"Please input your last name!": "请输入您的姓氏!",
|
||||
"Please input your phone number!": "请输入您的手机号码!",
|
||||
"Please input your real name!": "请输入您的姓名!",
|
||||
@@ -957,7 +954,6 @@
|
||||
"ID card type": "身份证类型",
|
||||
"ID card type - Tooltip": "身份证类型 - Tooltip",
|
||||
"ID card with person": "手持身份证",
|
||||
"Input your chat id": "Input your chat id",
|
||||
"Input your email": "请输入邮箱",
|
||||
"Input your phone number": "输入手机号",
|
||||
"Is admin": "是组织管理员",
|
||||
|
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
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 PlanBackend from "../backend/PlanBackend";
|
||||
import CustomGithubCorner from "../common/CustomGithubCorner";
|
||||
@@ -24,13 +24,17 @@ import i18next from "i18next";
|
||||
class PricingPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
this.state = {
|
||||
classes: props,
|
||||
applications: null,
|
||||
owner: props.owner ?? (props.match?.params?.owner ?? null),
|
||||
pricingName: (props.pricingName ?? props.match?.params?.pricingName) ?? null,
|
||||
userName: params.get("user"),
|
||||
pricing: props.pricing,
|
||||
plans: null,
|
||||
periods: null,
|
||||
selectedPeriod: null,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
@@ -39,7 +43,9 @@ class PricingPage extends React.Component {
|
||||
this.setState({
|
||||
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) {
|
||||
this.loadPlans();
|
||||
} else {
|
||||
@@ -60,7 +66,7 @@ class PricingPage extends React.Component {
|
||||
|
||||
loadPlans() {
|
||||
const plans = this.state.pricing.plans.map((plan) =>
|
||||
PlanBackend.getPlanById(plan, true));
|
||||
PlanBackend.getPlan(this.state.owner, plan, true));
|
||||
|
||||
Promise.all(plans)
|
||||
.then(results => {
|
||||
@@ -69,8 +75,12 @@ class PricingPage extends React.Component {
|
||||
Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
|
||||
return;
|
||||
}
|
||||
const plans = results.map(result => result.data);
|
||||
const periods = [... new Set(plans.map(plan => plan.period).filter(period => period !== ""))];
|
||||
this.setState({
|
||||
plans: results,
|
||||
plans: plans,
|
||||
periods: periods,
|
||||
selectedPeriod: periods?.[0],
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
@@ -80,17 +90,15 @@ class PricingPage extends React.Component {
|
||||
}
|
||||
|
||||
loadPricing(pricingName) {
|
||||
if (pricingName === undefined) {
|
||||
if (!pricingName) {
|
||||
return;
|
||||
}
|
||||
|
||||
PricingBackend.getPricing(this.state.owner, pricingName)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
pricing: res.data,
|
||||
@@ -103,11 +111,37 @@ class PricingPage extends React.Component {
|
||||
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 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}`;
|
||||
};
|
||||
|
||||
@@ -116,9 +150,9 @@ class PricingPage extends React.Component {
|
||||
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
|
||||
{
|
||||
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} />
|
||||
);
|
||||
) : null;
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
@@ -129,9 +163,9 @@ class PricingPage extends React.Component {
|
||||
<Row style={{justifyContent: "center"}} gutter={24}>
|
||||
{
|
||||
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} />
|
||||
);
|
||||
) : null;
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
@@ -155,6 +189,13 @@ class PricingPage extends React.Component {
|
||||
<div className="login-form">
|
||||
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
|
||||
<span style={{fontSize: "20px"}}>{pricing.description}</span>
|
||||
<Row style={{width: "100%", marginTop: "40px"}}>
|
||||
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||
{
|
||||
this.renderSelectPeriod()
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{width: "100%", marginTop: "40px"}}>
|
||||
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||
{
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import {Button, Card, Col} from "antd";
|
||||
import {Button, Card, Col, Row} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import {withRouter} from "react-router-dom";
|
||||
|
||||
@@ -29,49 +29,49 @@ class SingleCard extends React.Component {
|
||||
}
|
||||
|
||||
renderCard(plan, isSingle, link) {
|
||||
|
||||
return (
|
||||
<Col style={{minWidth: "320px", paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px", paddingTop: "0px"}} span={6}>
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => Setting.isMobile() ? window.location.href = link : null}
|
||||
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
|
||||
title={<h2>{plan.displayName}</h2>}
|
||||
>
|
||||
<div style={{textAlign: "right"}}>
|
||||
<h2
|
||||
style={{marginTop: "0px"}}>{plan.displayName}</h2>
|
||||
</div>
|
||||
<Col>
|
||||
<Row>
|
||||
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
||||
<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">
|
||||
<span style={{fontWeight: 700, fontSize: "48px"}}>$ {plan.pricePerMonth}</span>
|
||||
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span>
|
||||
</div>
|
||||
<Row style={{height: "90px", paddingTop: "15px"}}>
|
||||
<div style={{textAlign: "left", fontSize: "18px"}}>
|
||||
<Meta description={plan.description} />
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<br />
|
||||
<div style={{textAlign: "left", fontSize: "18px"}}>
|
||||
<Meta description={plan.description} />
|
||||
</div>
|
||||
<br />
|
||||
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
||||
{(plan.options ?? []).map((option) => {
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
return <li>
|
||||
<svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path>
|
||||
</svg>
|
||||
<span style={{fontSize: "16px"}}>{option}</span>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
<div style={{minHeight: "60px"}}>
|
||||
{/* <ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
||||
{(plan.options ?? []).map((option) => {
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
return <li>
|
||||
<svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path>
|
||||
</svg>
|
||||
<span style={{fontSize: "16px"}}>{option}</span>
|
||||
</li>;
|
||||
})}
|
||||
</ul> */}
|
||||
|
||||
</div>
|
||||
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
|
||||
{
|
||||
i18next.t("pricing:Getting started")
|
||||
}
|
||||
</Button>
|
||||
<Row style={{paddingTop: "15px"}}>
|
||||
<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")
|
||||
}
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
|
@@ -41,7 +41,7 @@ class PolicyTable extends React.Component {
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
if (this.props.mode === "edit") {
|
||||
if (this.props.mode === "edit" && this.props.enforcer.adapter !== "") {
|
||||
this.getPolicies();
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class PolicyTable extends React.Component {
|
||||
AdapterBackend.getPolicies(this.props.enforcer.owner, this.props.enforcer.name)
|
||||
.then((res) => {
|
||||
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;
|
||||
policyList.map((policy, index) => {
|
||||
@@ -175,7 +175,7 @@ class PolicyTable extends React.Component {
|
||||
render: (text, record, index) => {
|
||||
const editing = this.isEditing(index);
|
||||
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 => {
|
||||
this.updateField(table, index, "Ptype", value);
|
||||
}} />
|
||||
@@ -186,7 +186,7 @@ class PolicyTable extends React.Component {
|
||||
];
|
||||
|
||||
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) => {
|
||||
columns.push({
|
||||
title: title,
|
||||
@@ -209,7 +209,7 @@ class PolicyTable extends React.Component {
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "130px",
|
||||
width: "150px",
|
||||
render: (text, record, index) => {
|
||||
const editable = this.isEditing(index);
|
||||
return editable ? (
|
||||
@@ -247,7 +247,7 @@ class PolicyTable extends React.Component {
|
||||
loading={this.state.loading}
|
||||
title={() => (
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
@@ -257,7 +257,7 @@ class PolicyTable extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<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")}
|
||||
</Button>
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user