Compare commits

...

14 Commits

Author SHA1 Message Date
9f7924a6e0 fix: mask email and phone number on the backend (#563)
* fix: mask email and phone number on the backend

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: login with masked email or phone

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: improve regex

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-03-15 12:54:57 +08:00
377e200837 fix: repair the problem that AutoSigninFilter middleware doesn't recognize the access_token request parameter (#569)
AutoSigninFilter method only checks for `accessToken` request parameters or `Authorization` request header, doesn't recognize `access_token` request parameters, now added, use `utils.GetMaxLenStr()` method to get the maximum length characters
2022-03-15 12:52:44 +08:00
93a76de044 fix: fix compile error in low go version (#568) 2022-03-15 12:49:12 +08:00
35bef969fd feat: support Huawei Cloud SMS (#565)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-03-14 20:49:03 +08:00
4dca3bd3f7 Add Notify() to payment provider. 2022-03-14 02:56:04 +08:00
5de417ecf7 Add gc provider. 2022-03-14 00:32:36 +08:00
bf24594fb4 Make resource name longer. 2022-03-13 21:20:00 +08:00
4a87b4790e Avoid panic in AddUsers(). 2022-03-13 20:53:05 +08:00
fde8c4b5f6 Fix NotifyPayment(). 2022-03-13 19:57:23 +08:00
55a84644e1 Add PaymentResultPage. 2022-03-13 18:05:16 +08:00
ca87dd7dea Add returnUrl to product. 2022-03-13 16:25:54 +08:00
32af4a766e Add GetUserPayments() API. 2022-03-13 14:56:21 +08:00
4d035bf66d Add tags to organization. 2022-03-13 00:35:49 +08:00
743dcc9725 Fix translation. 2022-03-12 23:37:58 +08:00
49 changed files with 882 additions and 166 deletions

View File

@ -88,6 +88,10 @@ p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-user, *, *
p, *, *, GET, /api/get-user-application, *, *
p, *, *, GET, /api/get-resources, *, *
p, *, *, GET, /api/get-product, *, *
p, *, *, POST, /api/buy-product, *, *
p, *, *, GET, /api/get-payment, *, *
p, *, *, GET, /api/get-providers, *, *
p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, *

View File

@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
@ -172,6 +173,13 @@ func (c *ApiController) Signup() {
Karma: 0,
}
if len(organization.Tags) > 0 {
tokens := strings.Split(organization.Tags[0], "|")
if len(tokens) > 0 {
user.Tag = tokens[0]
}
}
if application.GetSignupItemRule("Display name") == "First, last" {
if form.FirstName != "" || form.LastName != "" {
user.DisplayName = fmt.Sprintf("%s %s", form.FirstName, form.LastName)

View File

@ -167,9 +167,16 @@ func (c *ApiController) Login() {
var verificationCodeType string
var checkResult string
if form.Name != "" {
user = object.GetUserByFields(form.Organization, form.Name)
}
// check result through Email or Phone
if strings.Contains(form.Username, "@") {
verificationCodeType = "email"
if user != nil && util.GetMaskedEmail(user.Email) == form.Username {
form.Username = user.Email
}
checkResult = object.CheckVerificationCode(form.Username, form.Code)
} else {
verificationCodeType = "phone"
@ -178,6 +185,9 @@ func (c *ApiController) Login() {
c.ResponseError(responseText)
return
}
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.Username = user.Phone
}
checkPhone := fmt.Sprintf("+%s%s", form.PhonePrefix, form.Username)
checkResult = object.CheckVerificationCode(checkPhone, form.Code)
}

View File

@ -21,7 +21,6 @@ import (
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay/alipay"
)
// GetPayments
@ -50,6 +49,24 @@ func (c *ApiController) GetPayments() {
}
}
// GetUserPayments
// @Title GetUserPayments
// @Tag Payment API
// @Description get payments for a user
// @Param owner query string true "The owner of payments"
// @Param organization query string true "The organization of the user"
// @Param user query string true "The username of the user"
// @Success 200 {array} object.Payment The Response object
// @router /get-user-payments [get]
func (c *ApiController) GetUserPayments() {
owner := c.Input().Get("owner")
organization := c.Input().Get("organization")
user := c.Input().Get("user")
payments := object.GetUserPayments(owner, organization, user)
c.ResponseOk(payments)
}
// @Title GetPayment
// @Tag Payment API
// @Description get payment
@ -124,14 +141,16 @@ func (c *ApiController) DeletePayment() {
// @Success 200 {object} controllers.Response The Response object
// @router /notify-payment [post]
func (c *ApiController) NotifyPayment() {
bm, err := alipay.ParseNotifyToBodyMap(c.Ctx.Request)
if err != nil {
panic(err)
}
owner := c.Ctx.Input.Param(":owner")
providerName := c.Ctx.Input.Param(":provider")
productName := c.Ctx.Input.Param(":product")
paymentName := c.Ctx.Input.Param(":payment")
ok := object.NotifyPayment(bm)
body := c.Ctx.Input.RequestBody
ok := object.NotifyPayment(c.Ctx.Request, body, owner, providerName, productName, paymentName)
if ok {
_, err = c.Ctx.ResponseWriter.Write([]byte("success"))
_, err := c.Ctx.ResponseWriter.Write([]byte("success"))
if err != nil {
panic(err)
}

View File

@ -119,13 +119,13 @@ func (c *ApiController) DeleteProduct() {
// @Title BuyProduct
// @Tag Product API
// @Description buy product
// @Param id query string true "The id of the product"
// @Param providerId query string true "The id of the provider"
// @Param id query string true "The id of the product"
// @Param providerName query string true "The name of the provider"
// @Success 200 {object} controllers.Response The Response object
// @router /buy-product [post]
func (c *ApiController) BuyProduct() {
id := c.Input().Get("id")
providerId := c.Input().Get("providerId")
providerName := c.Input().Get("providerName")
host := c.Ctx.Request.Host
userId := c.GetSessionUsername()
@ -140,7 +140,7 @@ func (c *ApiController) BuyProduct() {
return
}
payUrl, err := object.BuyProduct(id, providerId, user, host)
payUrl, err := object.BuyProduct(id, providerName, user, host)
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -194,15 +194,19 @@ func (c *ApiController) GetEmailAndPhone() {
return
}
respUser := object.User{Email: user.Email, Phone: user.Phone, Name: user.Name}
respUser := object.User{Name: user.Name}
var contentType string
switch form.Username {
case user.Email:
contentType = "email"
respUser.Email = user.Email
case user.Phone:
contentType = "phone"
respUser.Phone = user.Phone
case user.Name:
contentType = "username"
respUser.Email = util.GetMaskedEmail(user.Email)
respUser.Phone = util.GetMaskedPhone(user.Phone)
}
c.ResponseOk(respUser, contentType)

View File

@ -74,8 +74,16 @@ func (c *ApiController) SendVerificationCode() {
}
sendResp := errors.New("Invalid dest type")
if user == nil && checkUser != "" && checkUser != "true" {
_, name := util.GetOwnerAndNameFromId(orgId)
user = object.GetUser(fmt.Sprintf("%s/%s", name, checkUser))
}
switch destType {
case "email":
if user != nil && util.GetMaskedEmail(user.Email) == dest {
dest = user.Email
}
if !util.IsEmailValid(dest) {
c.ResponseError("Invalid Email address")
return
@ -84,6 +92,9 @@ func (c *ApiController) SendVerificationCode() {
provider := application.GetEmailProvider()
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
case "phone":
if user != nil && util.GetMaskedPhone(user.Phone) == dest {
dest = user.Phone
}
if !util.IsPhoneCnValid(dest) {
c.ResponseError("Invalid phone number")
return

2
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/casbin/casbin/v2 v2.30.1
github.com/casbin/xorm-adapter/v2 v2.5.1
github.com/casdoor/go-sms-sender v0.0.5
github.com/casdoor/go-sms-sender v0.2.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-ldap/ldap/v3 v3.3.0

2
go.sum
View File

@ -81,6 +81,8 @@ github.com/casbin/xorm-adapter/v2 v2.5.1 h1:BkpIxRHKa0s3bSMx173PpuU7oTs+Zw7XmD0B
github.com/casbin/xorm-adapter/v2 v2.5.1/go.mod h1:AeH4dBKHC9/zYxzdPVHhPDzF8LYLqjDdb767CWJoV54=
github.com/casdoor/go-sms-sender v0.0.5 h1:9qhlMM+UoSOvvY7puUULqSHBBA7fbe02Px/tzchQboo=
github.com/casdoor/go-sms-sender v0.0.5/go.mod h1:TMM/BsZQAa+7JVDXl2KqgxnzZgCjmHEX5MBN662mM5M=
github.com/casdoor/go-sms-sender v0.2.0 h1:52bin4EBOPzOee64s9UK7jxd22FODvT9/+Y/Z+PSHpg=
github.com/casdoor/go-sms-sender v0.2.0/go.mod h1:fsZsNnALvFIo+HFcE1U/oCQv4ZT42FdglXKMsEm3WSk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

View File

@ -47,9 +47,10 @@ func initBuiltInOrganization() {
DisplayName: "Built-in Organization",
WebsiteUrl: "https://example.com",
Favicon: "https://cdn.casbin.com/static/favicon.ico",
PasswordType: "plain",
PhonePrefix: "86",
DefaultAvatar: "https://casbin.org/img/casbin.svg",
PasswordType: "plain",
Tags: []string{},
}
AddOrganization(organization)
}

View File

@ -25,15 +25,16 @@ type Organization struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
Tags []string `xorm:"mediumtext" json:"tags"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
}
func GetOrganizationCount(owner, field, value string) int {

View File

@ -16,10 +16,9 @@ package object
import (
"fmt"
"net/http"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
"xorm.io/core"
)
@ -29,17 +28,22 @@ type Payment struct {
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Provider string `xorm:"varchar(100)" json:"provider"`
Type string `xorm:"varchar(100)" json:"type"`
Organization string `xorm:"varchar(100)" json:"organization"`
User string `xorm:"varchar(100)" json:"user"`
ProductId string `xorm:"varchar(100)" json:"productId"`
ProductName string `xorm:"varchar(100)" json:"productName"`
Price float64 `json:"price"`
Currency string `xorm:"varchar(100)" json:"currency"`
Provider string `xorm:"varchar(100)" json:"provider"`
Type string `xorm:"varchar(100)" json:"type"`
Organization string `xorm:"varchar(100)" json:"organization"`
User string `xorm:"varchar(100)" json:"user"`
ProductName string `xorm:"varchar(100)" json:"productName"`
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
State string `xorm:"varchar(100)" json:"state"`
Detail string `xorm:"varchar(100)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"`
Message string `xorm:"varchar(1000)" json:"message"`
}
func GetPaymentCount(owner, field, value string) int {
@ -62,6 +66,16 @@ func GetPayments(owner string) []*Payment {
return payments
}
func GetUserPayments(owner string, organization string, user string) []*Payment {
payments := []*Payment{}
err := adapter.Engine.Desc("created_time").Find(&payments, &Payment{Owner: owner, Organization: organization, User: user})
if err != nil {
panic(err)
}
return payments
}
func GetPaginationPayments(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Payment {
payments := []*Payment{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
@ -128,63 +142,59 @@ func DeletePayment(payment *Payment) bool {
return affected != 0
}
func NotifyPayment(bm gopay.BodyMap) bool {
owner := "admin"
productName := bm.Get("subject")
paymentId := bm.Get("out_trade_no")
priceString := bm.Get("total_amount")
price := util.ParseFloat(priceString)
productId := bm.Get("productId")
providerId := bm.Get("providerId")
product := getProduct(owner, productId)
if product == nil {
panic(fmt.Errorf("the product: %s does not exist", productId))
func notifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) (*Payment, error) {
payment := getPayment(owner, paymentName)
if payment == nil {
return nil, fmt.Errorf("the payment: %s does not exist", paymentName)
}
if productName != product.DisplayName {
panic(fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", productName, product.DisplayName))
product := getProduct(owner, productName)
if product == nil {
return nil, fmt.Errorf("the product: %s does not exist", productName)
}
provider, err := product.getProvider(providerName)
if err != nil {
return payment, err
}
pProvider, cert, err := provider.getPaymentProvider()
if err != nil {
return payment, err
}
productDisplayName, paymentName, price, productName, providerName, err := pProvider.Notify(request, body, cert.AuthorityPublicKey)
if err != nil {
return payment, err
}
if productDisplayName != "" && productDisplayName != product.DisplayName {
return nil, fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", productDisplayName, product.DisplayName)
}
if price != product.Price {
panic(fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", price, product.Price))
return nil, fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", price, product.Price)
}
payment := getPayment(owner, paymentId)
if payment == nil {
panic(fmt.Errorf("the payment: %s does not exist", paymentId))
}
return payment, nil
}
provider, err := product.getProvider(providerId)
if err != nil {
panic(err)
}
func NotifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) bool {
payment, err := notifyPayment(request, body, owner, providerName, productName, paymentName)
cert := getCert(owner, provider.Cert)
if cert == nil {
panic(fmt.Errorf("the cert: %s does not exist", provider.Cert))
}
ok, err := alipay.VerifySignWithCert(cert.AuthorityPublicKey, bm)
if err != nil {
panic(err)
}
if ok {
payment.State = "Paid"
} else {
if cert == nil {
panic(fmt.Errorf("VerifySignWithCert() failed: %v", ok))
if payment != nil {
if err != nil {
payment.State = "Error"
payment.Message = err.Error()
} else {
payment.State = "Paid"
}
//payment.State = "Failed"
UpdatePayment(payment.GetId(), payment)
}
affected, err := adapter.Engine.ID(core.PK{owner, paymentId}).AllCols().Update(payment)
if err != nil {
panic(err)
}
return affected != 0
ok := err == nil
return ok
}
func (payment *Payment) GetId() string {

View File

@ -17,7 +17,6 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -36,6 +35,7 @@ type Product struct {
Quantity int `json:"quantity"`
Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"`
}
@ -152,55 +152,54 @@ func (product *Product) getProvider(providerId string) (*Provider, error) {
return provider, nil
}
func BuyProduct(id string, providerId string, user *User, host string) (string, error) {
func BuyProduct(id string, providerName string, user *User, host string) (string, error) {
product := GetProduct(id)
if product == nil {
return "", fmt.Errorf("the product: %s does not exist", id)
}
provider, err := product.getProvider(providerId)
provider, err := product.getProvider(providerName)
if err != nil {
return "", err
}
cert := getCert(product.Owner, provider.Cert)
if cert == nil {
return "", fmt.Errorf("the cert: %s does not exist", provider.Cert)
pProvider, _, err := provider.getPaymentProvider()
if err != nil {
return "", err
}
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
if pProvider == nil {
return "", fmt.Errorf("the payment provider type: %s is not supported", provider.Type)
}
paymentId := util.GenerateTimeId()
productName := product.DisplayName
productId := product.Name
owner := product.Owner
productName := product.Name
paymentName := util.GenerateTimeId()
productDisplayName := product.DisplayName
originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s", originFrontend, paymentId)
notifyUrl := fmt.Sprintf("%s/api/notify-payment", originBackend)
returnUrl := fmt.Sprintf("%s/payments/%s/result", originFrontend, paymentName)
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s/%s/%s", originBackend, owner, providerName, productName, paymentName)
payUrl, err := pProvider.Pay(productName, productId, providerId, paymentId, product.Price, returnUrl, notifyUrl)
payUrl, err := pProvider.Pay(providerName, productName, paymentName, productDisplayName, product.Price, returnUrl, notifyUrl)
if err != nil {
return "", err
}
payment := Payment{
Owner: product.Owner,
Name: paymentId,
CreatedTime: util.GetCurrentTime(),
DisplayName: paymentId,
Provider: provider.Name,
Type: provider.Type,
Organization: user.Owner,
User: user.Name,
ProductId: productId,
ProductName: productName,
Price: product.Price,
Currency: product.Currency,
PayUrl: payUrl,
State: "Created",
Owner: product.Owner,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
DisplayName: paymentName,
Provider: provider.Name,
Type: provider.Type,
Organization: user.Owner,
User: user.Name,
ProductName: productName,
ProductDisplayName: productDisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,
Price: product.Price,
PayUrl: payUrl,
ReturnUrl: product.ReturnUrl,
State: "Created",
}
affected := AddPayment(&payment)
if !affected {

View File

@ -29,7 +29,7 @@ func TestProduct(t *testing.T) {
product := GetProduct("admin/product_123")
provider := getProvider(product.Owner, "provider_pay_alipay")
cert := getCert(product.Owner, "cert-pay-alipay")
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
paymentId := util.GenerateTimeId()
returnUrl := ""

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -182,6 +183,23 @@ func DeleteProvider(provider *Provider) bool {
return affected != 0
}
func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
cert := &Cert{}
if p.Cert != "" {
cert = getCert(p.Owner, p.Cert)
if cert == nil {
return nil, nil, fmt.Errorf("the cert: %s does not exist", p.Cert)
}
}
pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
if pProvider == nil {
return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
}
return pProvider, cert, nil
}
func (p *Provider) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

View File

@ -23,7 +23,7 @@ import (
type Resource struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
Name string `xorm:"varchar(200) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
User string `xorm:"varchar(100)" json:"user"`

View File

@ -18,6 +18,9 @@ import "github.com/casdoor/go-sms-sender"
func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
client, err := go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
if provider.Type == go_sms_sender.HuaweiCloud {
client, err = go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
}
if err != nil {
return err
}

View File

@ -380,7 +380,9 @@ func AddUsers(users []*User) bool {
affected, err := adapter.Engine.Insert(users)
if err != nil {
panic(err)
if !strings.Contains(err.Error(), "Duplicate entry") {
panic(err)
}
}
return affected != 0

View File

@ -17,8 +17,9 @@ package pp
import (
"context"
"fmt"
"strings"
"net/http"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
)
@ -44,20 +45,20 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
return pp
}
func (pp *AlipayPaymentProvider) Pay(productName string, productId string, providerId string, paymentId string, price float64, returnUrl string, notifyUrl string) (string, error) {
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
//pp.Client.DebugSwitch = gopay.DebugOn
priceString := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", price), "0"), ".")
bm := gopay.BodyMap{}
bm.Set("subject", productName)
bm.Set("out_trade_no", paymentId)
bm.Set("total_amount", priceString)
bm.Set("providerName", providerName)
bm.Set("productName", productName)
bm.Set("return_url", returnUrl)
bm.Set("notify_url", notifyUrl)
bm.Set("productId", productId)
bm.Set("providerId", productId)
bm.Set("subject", productDisplayName)
bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", getPriceString(price))
payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil {
@ -65,3 +66,27 @@ func (pp *AlipayPaymentProvider) Pay(productName string, productId string, provi
}
return payUrl, nil
}
func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
bm, err := alipay.ParseNotifyToBodyMap(request)
if err != nil {
return "", "", 0, "", "", err
}
providerName := bm.Get("providerName")
productName := bm.Get("productName")
productDisplayName := bm.Get("subject")
paymentName := bm.Get("out_trade_no")
price := util.ParseFloat(bm.Get("total_amount"))
ok, err := alipay.VerifySignWithCert(authorityPublicKey, bm)
if err != nil {
return "", "", 0, "", "", err
}
if !ok {
return "", "", 0, "", "", fmt.Errorf("VerifySignWithCert() failed: %v", ok)
}
return productDisplayName, paymentName, price, productName, providerName, nil
}

232
pp/gc.go Normal file
View File

@ -0,0 +1,232 @@
// Copyright 2022 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 pp
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/casdoor/casdoor/util"
)
type GcPaymentProvider struct {
Xmpch string
SecretKey string
Host string
}
type GcPayReqInfo struct {
OrderDate string `json:"orderdate"`
OrderNo string `json:"orderno"`
Amount string `json:"amount"`
PayerId string `json:"payerid"`
PayerName string `json:"payername"`
Xmpch string `json:"xmpch"`
ReturnUrl string `json:"return_url"`
NotifyUrl string `json:"notify_url"`
}
type GcPayRespInfo struct {
Jylsh string `json:"jylsh"`
Amount string `json:"amount"`
PayerId string `json:"payerid"`
PayerName string `json:"payername"`
PayUrl string `json:"payurl"`
}
type GcNotifyRespInfo struct {
Xmpch string `json:"xmpch"`
OrderDate string `json:"orderdate"`
OrderNo string `json:"orderno"`
Amount float64 `json:"amount"`
Jylsh string `json:"jylsh"`
TradeNo string `json:"tradeno"`
PayMethod string `json:"paymethod"`
OrderState string `json:"orderstate"`
ReturnType string `json:"return_type"`
PayerId string `json:"payerid"`
PayerName string `json:"payername"`
}
type GcRequestBody struct {
Op string `json:"op"`
Xmpch string `json:"xmpch"`
Version string `json:"version"`
Data string `json:"data"`
RequestTime string `json:"requesttime"`
Sign string `json:"sign"`
}
type GcResponseBody struct {
Op string `json:"op"`
Xmpch string `json:"xmpch"`
Version string `json:"version"`
ReturnCode string `json:"return_code"`
ReturnMsg string `json:"return_msg"`
Data string `json:"data"`
NotifyTime string `json:"notifytime"`
Sign string `json:"sign"`
}
func NewGcPaymentProvider(clientId string, clientSecret string, host string) *GcPaymentProvider {
pp := &GcPaymentProvider{}
pp.Xmpch = clientId
pp.SecretKey = clientSecret
pp.Host = host
return pp
}
func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
client := &http.Client{}
var resp *http.Response
var err error
contentType := "text/plain;charset=UTF-8"
body := bytes.NewReader(postBytes)
req, err := http.NewRequest("POST", pp.Host, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
resp, err = client.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respBytes, nil
}
func (pp *GcPaymentProvider) Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(),
OrderNo: util.GenerateTimeId(),
Amount: getPriceString(price),
PayerId: "",
PayerName: "",
Xmpch: pp.Xmpch,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
}
b, err := json.Marshal(payReqInfo)
if err != nil {
return "", err
}
body := GcRequestBody{
Op: "OrderCreate",
Xmpch: pp.Xmpch,
Version: "1.4",
Data: base64.StdEncoding.EncodeToString(b),
RequestTime: util.GenerateSimpleTimeId(),
}
params := fmt.Sprintf("data=%s&op=%s&requesttime=%s&version=%s&xmpch=%s%s", body.Data, body.Op, body.RequestTime, body.Version, body.Xmpch, pp.SecretKey)
body.Sign = strings.ToUpper(util.GetMd5Hash(params))
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", err
}
respBytes, err := pp.doPost(bodyBytes)
if err != nil {
return "", err
}
var respBody GcResponseBody
err = json.Unmarshal(respBytes, &respBody)
if err != nil {
return "", err
}
if respBody.ReturnCode != "SUCCESS" {
return "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
}
payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
if err != nil {
return "", err
}
var payRespInfo GcPayRespInfo
err = json.Unmarshal(payRespInfoBytes, &payRespInfo)
if err != nil {
return "", err
}
return payRespInfo.PayUrl, nil
}
func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
reqBody := GcRequestBody{}
m, err := url.ParseQuery(string(body))
if err != nil {
return "", "", 0, "", "", err
}
reqBody.Op = m["op"][0]
reqBody.Xmpch = m["xmpch"][0]
reqBody.Version = m["version"][0]
reqBody.Data = m["data"][0]
reqBody.RequestTime = m["requesttime"][0]
reqBody.Sign = m["sign"][0]
notifyReqInfoBytes, err := base64.StdEncoding.DecodeString(reqBody.Data)
if err != nil {
return "", "", 0, "", "", err
}
var notifyRespInfo GcNotifyRespInfo
err = json.Unmarshal(notifyReqInfoBytes, &notifyRespInfo)
if err != nil {
return "", "", 0, "", "", err
}
providerName := ""
productName := ""
productDisplayName := ""
paymentName := notifyRespInfo.OrderNo
price := notifyRespInfo.Amount
if notifyRespInfo.OrderState != "1" {
return "", "", 0, "", "", fmt.Errorf("error order state: %s", notifyRespInfo.OrderDate)
}
return productDisplayName, paymentName, price, productName, providerName, nil
}

View File

@ -14,13 +14,18 @@
package pp
import "net/http"
type PaymentProvider interface {
Pay(productName string, productId string, providerId string, paymentId string, price float64, returnUrl string, notifyUrl string) (string, error)
Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error)
Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error)
}
func GetPaymentProvider(typ string, appId string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {
if typ == "Alipay" {
return NewAlipayPaymentProvider(appId, appPublicKey, appPrivateKey, authorityPublicKey, authorityRootPublicKey)
} else if typ == "GC" {
return NewGcPaymentProvider(appId, clientSecret, host)
}
return nil
}

25
pp/util.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2022 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 pp
import (
"fmt"
"strings"
)
func getPriceString(price float64) string {
priceString := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", price), "0"), ".")
return priceString
}

View File

@ -29,10 +29,8 @@ func AutoSigninFilter(ctx *context.Context) {
// GET parameter like "/page?access_token=123" or
// HTTP Bearer token like "Authorization: Bearer 123"
accessToken := ctx.Input.Query("accessToken")
if accessToken == "" {
accessToken = parseBearerToken(ctx)
}
accessToken := util.GetMaxLenStr(ctx.Input.Query("accessToken"), ctx.Input.Query("access_token"), parseBearerToken(ctx))
if accessToken != "" {
token := object.GetTokenByAccessToken(accessToken)
if token == nil {

View File

@ -159,11 +159,12 @@ func initAPI() {
beego.Router("/api/buy-product", &controllers.ApiController{}, "POST:BuyProduct")
beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments")
beego.Router("/api/get-user-payments", &controllers.ApiController{}, "GET:GetUserPayments")
beego.Router("/api/get-payment", &controllers.ApiController{}, "GET:GetPayment")
beego.Router("/api/update-payment", &controllers.ApiController{}, "POST:UpdatePayment")
beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment")
beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment")
beego.Router("/api/notify-payment", &controllers.ApiController{}, "POST:NotifyPayment")
beego.Router("/api/notify-payment/?:owner/?:provider/?:product/?:payment", &controllers.ApiController{}, "POST:NotifyPayment")
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")

View File

@ -20,10 +20,12 @@ import (
)
var rePhoneCn *regexp.Regexp
var rePhone *regexp.Regexp
func init() {
// https://learnku.com/articles/31543
rePhoneCn, _ = regexp.Compile(`^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$`)
rePhone, _ = regexp.Compile("(\\d{3})\\d*(\\d{4})")
}
func IsEmailValid(email string) bool {
@ -34,3 +36,7 @@ func IsEmailValid(email string) bool {
func IsPhoneCnValid(phone string) bool {
return rePhoneCn.MatchString(phone)
}
func getMaskedPhone(phone string) string {
return rePhone.ReplaceAllString(phone, "$1****$2")
}

View File

@ -109,6 +109,14 @@ func GenerateTimeId() string {
return res
}
func GenerateSimpleTimeId() string {
timestamp := time.Now().Unix()
tm := time.Unix(timestamp, 0)
t := tm.Format("20060102150405")
return t
}
func GetId(name string) string {
return fmt.Sprintf("admin/%s", name)
}
@ -198,3 +206,28 @@ func IsChinese(str string) bool {
}
return flag
}
func GetMaskedPhone(phone string) string {
return getMaskedPhone(phone)
}
func GetMaskedEmail(email string) string {
if email == "" {
return ""
}
tokens := strings.Split(email, "@")
username := maskString(tokens[0])
domain := tokens[1]
domainTokens := strings.Split(domain, ".")
domainTokens[len(domainTokens) - 2] = maskString(domainTokens[len(domainTokens) - 2])
return fmt.Sprintf("%s@%s", username, strings.Join(domainTokens, "."))
}
func maskString(str string) string {
if len(str) <= 2 {
return str
} else {
return fmt.Sprintf("%c%s%c", str[0], strings.Repeat("*", len(str) - 2), str[len(str) - 1])
}
}

View File

@ -48,6 +48,7 @@ import ProductEditPage from "./ProductEditPage";
import ProductBuyPage from "./ProductBuyPage";
import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage";
import PaymentResultPage from "./PaymentResultPage";
import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./CustomGithubCorner";
@ -529,6 +530,7 @@ class App extends Component {
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}

View File

@ -113,7 +113,7 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("general:Favicon", i18next.t("general:Favicon - Tooltip"))} :
{Setting.getLabel( i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: '20px'}} >
@ -208,6 +208,18 @@ class OrganizationEditPage extends React.Component {
</Row>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.organization.tags} onChange={(value => {this.updateOrganizationField('tags', value);})}>
{
this.state.organization.tags?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Master password"), i18next.t("general:Master password - Tooltip"))} :

View File

@ -36,6 +36,7 @@ class OrganizationListPage extends BaseListPage {
PasswordSalt: "",
phonePrefix: "86",
defaultAvatar: "https://casbin.org/img/casbin.svg",
tags: [],
masterPassword: "",
enableSoftDeletion: false,
}

View File

@ -160,6 +160,16 @@ class PaymentEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Message"), i18next.t("payment:Message - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.payment.message} onChange={e => {
// this.updatePaymentField('message', e.target.value);
}} />
</Col>
</Row>
</Card>
)
}

View File

@ -34,11 +34,16 @@ class PaymentListPage extends BaseListPage {
type: "PayPal",
organization: "built-in",
user: "admin",
productId: "computer-1",
productName: "A notebook computer",
price: 300.00,
productName: "computer-1",
productDisplayName: "A notebook computer",
detail: "This is a computer with excellent CPU, memory and disk",
tag: "Promotion-1",
currency: "USD",
price: 300.00,
payUrl: "https://pay.com/pay.php",
returnUrl: "https://door.casdoor.com/payments",
state: "Paid",
message: "",
}
}
@ -167,11 +172,11 @@ class PaymentListPage extends BaseListPage {
},
{
title: i18next.t("payment:Product"),
dataIndex: 'productName',
key: 'productName',
dataIndex: 'productDisplayName',
key: 'productDisplayName',
// width: '160px',
sorter: true,
...this.getColumnSearchProps('productName'),
...this.getColumnSearchProps('productDisplayName'),
},
{
title: i18next.t("payment:Price"),
@ -201,11 +206,12 @@ class PaymentListPage extends BaseListPage {
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
width: '240px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} onClick={() => this.props.history.push(`/payments/${record.name}/result`)}>{i18next.t("payment:Result")}</Button>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/payments/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete payment: ${record.name} ?`}

View File

@ -0,0 +1,115 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Result, Spin} from 'antd';
import * as PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
class PaymentResultPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
paymentName: props.match.params.paymentName,
payment: null,
};
}
UNSAFE_componentWillMount() {
this.getPayment();
}
getPayment() {
PaymentBackend.getPayment("admin", this.state.paymentName)
.then((payment) => {
this.setState({
payment: payment,
});
if (payment.state === "Created") {
setTimeout(() => this.getPayment(), 1000);
}
});
}
render() {
const payment = this.state.payment;
if (payment === null) {
return null;
}
if (payment.state === "Paid") {
return (
<div>
{
Setting.renderHelmet(payment)
}
<Result
status="success"
title={`${i18next.t("payment:You have successfully completed the payment")}: ${payment.productDisplayName}`}
subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[
<Button type="primary" key="returnUrl" onClick={() => {
Setting.goToLink(payment.returnUrl);
}}>
{i18next.t("payment:Return to Website")}
</Button>
]}
/>
</div>
)
} else if (payment.state === "Created") {
return (
<div>
{
Setting.renderHelmet(payment)
}
<Result
status="info"
title={`${i18next.t("payment:The payment is still under processing")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}, ${i18next.t("payment:please wait for a few seconds...")}`}
subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[
<Spin size="large" tip={i18next.t("payment:Processing...")} />,
]}
/>
</div>
)
} else {
return (
<div>
{
Setting.renderHelmet(payment)
}
<Result
status="error"
title={`${i18next.t("payment:The payment has failed")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[
<Button type="primary" key="returnUrl" onClick={() => {
Setting.goToLink(payment.returnUrl);
}}>
{i18next.t("payment:Return to Website")}
</Button>
]}
/>
</div>
)
}
}
}
export default PaymentResultPage;

View File

@ -186,6 +186,10 @@ class ProductBuyPage extends React.Component {
render() {
const product = this.getProductObj();
if (product === null) {
return null;
}
return (
<div>
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >

View File

@ -212,6 +212,16 @@ class ProductEditPage extends React.Component {
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Return URL"), i18next.t("product:Return URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined/>} value={this.state.product.returnUrl} onChange={e => {
this.updateProductField('returnUrl', 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"))} :

View File

@ -72,6 +72,8 @@ class ProviderEditPage extends React.Component {
case "SMS":
if (this.state.provider.type === "Volc Engine SMS")
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
if (this.state.provider.type === "Huawei Cloud SMS")
return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"));
default:
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
@ -84,6 +86,8 @@ class ProviderEditPage extends React.Component {
case "SMS":
if (this.state.provider.type === "Volc Engine SMS")
return Setting.getLabel(i18next.t("provider:Secret access key"), i18next.t("provider:SecretAccessKey - Tooltip"));
if (this.state.provider.type === "Huawei Cloud SMS")
return Setting.getLabel(i18next.t("provider:App secret"), i18next.t("provider:AppSecret - Tooltip"));
default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
@ -103,6 +107,9 @@ class ProviderEditPage extends React.Component {
} else if (this.state.provider.category === "SMS" && this.state.provider.type === "Volc Engine SMS") {
text = i18next.t("provider:SMS account");
tooltip = i18next.t("provider:SMS account - Tooltip");
} else if (this.state.provider.category === "SMS" && this.state.provider.type === "Huawei Cloud SMS") {
text = i18next.t("provider:Channel No.");
tooltip = i18next.t("provider:Channel No. - Tooltip");
} else {
return null;
}

View File

@ -423,6 +423,7 @@ export function getProviderTypeOptions(category) {
{id: 'Aliyun SMS', name: 'Aliyun SMS'},
{id: 'Tencent Cloud SMS', name: 'Tencent Cloud SMS'},
{id: 'Volc Engine SMS', name: 'Volc Engine SMS'},
{id: 'Huawei Cloud SMS', name: 'Huawei Cloud SMS'},
]
);
} else if (category === "Storage") {
@ -444,6 +445,7 @@ export function getProviderTypeOptions(category) {
{id: 'Alipay', name: 'Alipay'},
{id: 'WeChat Pay', name: 'WeChat Pay'},
{id: 'PayPal', name: 'PayPal'},
{id: 'GC', name: 'GC'},
]);
} else {
return [];

View File

@ -301,9 +301,24 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.tag} onChange={e => {
this.updateUserField('tag', e.target.value);
}} />
{
this.state.application?.organizationObj.tags?.length > 0 ? (
<Select virtual={false} style={{width: '100%'}} value={this.state.user.tag} onChange={(value => {this.updateUserField('tag', value);})}>
{
this.state.application.organizationObj.tags?.map((tag, index) => {
const tokens = tag.split("|");
const value = tokens[0];
const displayValue = Setting.getLanguage() !== "zh" ? tokens[0] : tokens[1];
return <Option key={index} value={value}>{displayValue}</Option>
})
}
</Select>
) : (
<Input value={this.state.user.tag} onChange={e => {
this.updateUserField('tag', e.target.value);
}} />
)
}
</Col>
</Row>
<Row style={{marginTop: '20px'}} >

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Col, Form, Select, Input, Row, Steps} from "antd";
import {Button, Col, Form, Input, Row, Select, Steps} from "antd";
import * as AuthBackend from "./AuthBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Util from "./Util";
@ -43,6 +43,7 @@ class ForgetPage extends React.Component {
msg: null,
userId: "",
username: "",
name: "",
email: "",
isFixed: false,
fixedContent: "",
@ -100,7 +101,7 @@ class ForgetPage extends React.Component {
if (res.status === "ok") {
const phone = res.data.phone;
const email = res.data.email;
this.setState({phone: phone, email: email, username: res.data.name});
this.setState({phone: phone, email: email, username: res.data.name, name: res.data.name});
if (phone !== "" && email === "") {
this.setState({
@ -134,15 +135,16 @@ class ForgetPage extends React.Component {
break;
case "step2":
const oAuthParams = Util.getOAuthGetParameters();
if(this.state.verifyType=="email"){
if (this.state.verifyType === "email") {
this.setState({username: this.state.email})
}else if(this.state.verifyType=="phone"){
} else if (this.state.verifyType === "phone") {
this.setState({username: this.state.phone})
}
AuthBackend.login({
application: forms.step2.getFieldValue("application"),
organization: forms.step2.getFieldValue("organization"),
username: this.state.username,
name: this.state.name,
code: forms.step2.getFieldValue("emailCode"),
phonePrefix: this.state.application?.organizationObj.phonePrefix,
type: "login"
@ -179,7 +181,7 @@ class ForgetPage extends React.Component {
if (this.state.phone !== "") {
options.push(
<Option key={"phone"} value={"phone"}>
&nbsp;&nbsp;{Setting.getMaskedPhone(this.state.phone)}
&nbsp;&nbsp;{this.state.phone}
</Option>
);
}
@ -187,7 +189,7 @@ class ForgetPage extends React.Component {
if (this.state.email !== "") {
options.push(
<Option key={"email"} value={"email"}>
&nbsp;&nbsp;{Setting.getMaskedEmail(this.state.email)}
&nbsp;&nbsp;{this.state.email}
</Option>
);
}
@ -349,12 +351,12 @@ class ForgetPage extends React.Component {
{this.state.verifyType === "email" ? (
<CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationOrgName(this.state.application)]}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationOrgName(this.state.application), this.state.name]}
/>
) : (
<CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationOrgName(this.state.application)]}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationOrgName(this.state.application), this.state.name]}
/>
)}
</Form.Item>

View File

@ -112,6 +112,10 @@ const otherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_volc_engine.jpg`,
url: "https://www.volcengine.com/products/cloud-sms",
},
"Huawei Cloud SMS": {
logo: `${StaticBaseUrl}/img/social_huawei.png`,
url: "https://www.huaweicloud.com/product/msgsms.html",
},
},
Email: {
"Default": {
@ -160,6 +164,10 @@ const otherProviderInfo = {
logo: `${StaticBaseUrl}/img/payment_paypal.png`,
url: "https://www.paypal.com/"
},
"GC": {
logo: `${StaticBaseUrl}/img/payment_gc.png`,
url: "https://gc.org"
},
},
};

View File

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

View File

@ -49,14 +49,6 @@ export const CountDownInput = (props) => {
const handleOk = () => {
setVisible(false);
if (isValidEmail(onButtonClickArgs[0])) {
onButtonClickArgs[1] = "email";
} else if (isValidPhone(onButtonClickArgs[0])) {
onButtonClickArgs[1] = "phone";
} else {
Util.showMessage("error", i18next.t("login:Invalid Email or phone"))
return;
}
setButtonLoading(true)
UserBackend.sendCode(checkType, checkId, key, ...onButtonClickArgs).then(res => {
setKey("");

View File

@ -113,6 +113,7 @@
"Edit": "Bearbeiten",
"Email": "E-Mail",
"Email - Tooltip": "email",
"Favicon": "Favicon",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URL vergessen",
@ -248,6 +249,8 @@
"New Organization": "New Organization",
"Soft deletion": "Weiche Löschung",
"Soft deletion - Tooltip": "Weiche Löschung - Tooltip",
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website-URL",
"Website URL - Tooltip": "Unique string-style identifier"
},
@ -256,12 +259,23 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"New Payment": "New Payment",
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Processing...": "Processing...",
"Product": "Product",
"Product - Tooltip": "Product - Tooltip",
"Result": "Result",
"Return to Website": "Return to Website",
"State": "State",
"State - Tooltip": "State - Tooltip",
"The payment has failed": "The payment has failed",
"The payment is still under processing": "The payment is still under processing",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
"Type - Tooltip": "Type - Tooltip",
"You have successfully completed the payment": "You have successfully completed the payment",
"please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is"
},
"permission": {
"Actions": "Aktionen",
@ -296,6 +310,8 @@
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Return URL": "Return URL",
"Return URL - Tooltip": "Return URL - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",

View File

@ -113,6 +113,7 @@
"Edit": "Edit",
"Email": "Email",
"Email - Tooltip": "Email - Tooltip",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon - Tooltip",
"First name": "First name",
"Forget URL": "Forget URL",
@ -248,6 +249,8 @@
"New Organization": "New Organization",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL",
"Website URL - Tooltip": "Website URL - Tooltip"
},
@ -256,12 +259,23 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"New Payment": "New Payment",
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Processing...": "Processing...",
"Product": "Product",
"Product - Tooltip": "Product - Tooltip",
"Result": "Result",
"Return to Website": "Return to Website",
"State": "State",
"State - Tooltip": "State - Tooltip",
"The payment has failed": "The payment has failed",
"The payment is still under processing": "The payment is still under processing",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
"Type - Tooltip": "Type - Tooltip",
"You have successfully completed the payment": "You have successfully completed the payment",
"please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is"
},
"permission": {
"Actions": "Actions",
@ -296,6 +310,8 @@
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Return URL": "Return URL",
"Return URL - Tooltip": "Return URL - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",

View File

@ -113,6 +113,7 @@
"Edit": "Editer",
"Email": "Courriel",
"Email - Tooltip": "email",
"Favicon": "Favicon",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Oublier l'URL",
@ -248,6 +249,8 @@
"New Organization": "New Organization",
"Soft deletion": "Suppression du logiciel",
"Soft deletion - Tooltip": "Suppression de soft - infobulle",
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "URL du site web",
"Website URL - Tooltip": "Unique string-style identifier"
},
@ -256,12 +259,23 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"New Payment": "New Payment",
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Processing...": "Processing...",
"Product": "Product",
"Product - Tooltip": "Product - Tooltip",
"Result": "Result",
"Return to Website": "Return to Website",
"State": "State",
"State - Tooltip": "State - Tooltip",
"The payment has failed": "The payment has failed",
"The payment is still under processing": "The payment is still under processing",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
"Type - Tooltip": "Type - Tooltip",
"You have successfully completed the payment": "You have successfully completed the payment",
"please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is"
},
"permission": {
"Actions": "Actions",
@ -296,6 +310,8 @@
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Return URL": "Return URL",
"Return URL - Tooltip": "Return URL - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",

View File

@ -113,6 +113,7 @@
"Edit": "編集",
"Email": "Eメールアドレス",
"Email - Tooltip": "email",
"Favicon": "Favicon",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URLを忘れた",
@ -248,6 +249,8 @@
"New Organization": "New Organization",
"Soft deletion": "ソフト削除",
"Soft deletion - Tooltip": "ソフト削除 - ツールチップ",
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL",
"Website URL - Tooltip": "Unique string-style identifier"
},
@ -256,12 +259,23 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"New Payment": "New Payment",
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Processing...": "Processing...",
"Product": "Product",
"Product - Tooltip": "Product - Tooltip",
"Result": "Result",
"Return to Website": "Return to Website",
"State": "State",
"State - Tooltip": "State - Tooltip",
"The payment has failed": "The payment has failed",
"The payment is still under processing": "The payment is still under processing",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
"Type - Tooltip": "Type - Tooltip",
"You have successfully completed the payment": "You have successfully completed the payment",
"please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is"
},
"permission": {
"Actions": "アクション",
@ -296,6 +310,8 @@
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Return URL": "Return URL",
"Return URL - Tooltip": "Return URL - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",

View File

@ -113,6 +113,7 @@
"Edit": "Edit",
"Email": "Email",
"Email - Tooltip": "email",
"Favicon": "Favicon",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Forget URL",
@ -248,6 +249,8 @@
"New Organization": "New Organization",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "Website URL",
"Website URL - Tooltip": "Unique string-style identifier"
},
@ -256,12 +259,23 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"New Payment": "New Payment",
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Processing...": "Processing...",
"Product": "Product",
"Product - Tooltip": "Product - Tooltip",
"Result": "Result",
"Return to Website": "Return to Website",
"State": "State",
"State - Tooltip": "State - Tooltip",
"The payment has failed": "The payment has failed",
"The payment is still under processing": "The payment is still under processing",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
"Type - Tooltip": "Type - Tooltip",
"You have successfully completed the payment": "You have successfully completed the payment",
"please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is"
},
"permission": {
"Actions": "Actions",
@ -296,6 +310,8 @@
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Return URL": "Return URL",
"Return URL - Tooltip": "Return URL - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",

View File

@ -113,6 +113,7 @@
"Edit": "Редактирование",
"Email": "Почта",
"Email - Tooltip": "email",
"Favicon": "Favicon",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Забыть URL",
@ -248,6 +249,8 @@
"New Organization": "New Organization",
"Soft deletion": "Мягкое удаление",
"Soft deletion - Tooltip": "Мягкое удаление - Подсказка",
"Tags": "Tags",
"Tags - Tooltip": "Tags - Tooltip",
"Website URL": "URL сайта",
"Website URL - Tooltip": "Unique string-style identifier"
},
@ -256,12 +259,23 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"New Payment": "New Payment",
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Processing...": "Processing...",
"Product": "Product",
"Product - Tooltip": "Product - Tooltip",
"Result": "Result",
"Return to Website": "Return to Website",
"State": "State",
"State - Tooltip": "State - Tooltip",
"The payment has failed": "The payment has failed",
"The payment is still under processing": "The payment is still under processing",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip"
"Type - Tooltip": "Type - Tooltip",
"You have successfully completed the payment": "You have successfully completed the payment",
"please wait for a few seconds...": "please wait for a few seconds...",
"the current state is": "the current state is"
},
"permission": {
"Actions": "Действия",
@ -296,6 +310,8 @@
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Return URL": "Return URL",
"Return URL - Tooltip": "Return URL - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",

View File

@ -113,7 +113,8 @@
"Edit": "编辑",
"Email": "电子邮箱",
"Email - Tooltip": "电子邮件:",
"Favicon - Tooltip": "网站图标",
"Favicon": "网站图标",
"Favicon - Tooltip": "网站的Favicon图标",
"First name": "名字",
"Forget URL": "忘记密码URL",
"Forget URL - Tooltip": "忘记密码URL",
@ -248,6 +249,8 @@
"New Organization": "添加组织",
"Soft deletion": "软删除",
"Soft deletion - Tooltip": "启用后,删除用户信息时不会在数据库彻底清除,只会标记为已删除状态",
"Tags": "标签集合",
"Tags - Tooltip": "可供用户选择的标签的集合",
"Website URL": "网页地址",
"Website URL - Tooltip": "网页地址"
},
@ -256,12 +259,23 @@
"Currency - Tooltip": "如USD美元CNY人民币等",
"Edit Payment": "编辑付款",
"New Payment": "添加付款",
"Please click the below button to return to the original website": "请点击下方按钮返回原网站",
"Price": "价格",
"Price - Tooltip": "商品价格",
"Processing...": "正在处理...",
"Product": "商品",
"Product - Tooltip": "商品名称",
"Result": "结果",
"Return to Website": "返回原网站",
"State": "状态",
"State - Tooltip": "交易状态",
"The payment has failed": "支付失败",
"The payment is still under processing": "支付正在处理",
"Type": "支付方式",
"Type - Tooltip": "商品购买时的支付方式"
"Type - Tooltip": "商品购买时的支付方式",
"You have successfully completed the payment": "支付成功",
"please wait for a few seconds...": "请稍后...",
"the current state is": "当前状态为"
},
"permission": {
"Actions": "动作",
@ -296,6 +310,8 @@
"Price - Tooltip": "价格 - 工具提示",
"Quantity": "库存",
"Quantity - Tooltip": "库存 - 工具提示",
"Return URL": "返回URL",
"Return URL - Tooltip": "返回URL - 工具提示",
"SKU": "货号",
"Sold": "售出",
"Sold - Tooltip": "售出 - 工具提示",

View File

@ -13,6 +13,7 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package xlsx