diff --git a/controllers/base.go b/controllers/base.go index 2e222089..203b120a 100644 --- a/controllers/base.go +++ b/controllers/base.go @@ -132,3 +132,11 @@ func wrapActionResponse(affected bool) *Response { return &Response{Status: "ok", Msg: "", Data: "Unaffected"} } } + +func wrapErrorResponse(err error) *Response { + if err == nil { + return &Response{Status: "ok", Msg: ""} + } else { + return &Response{Status: "error", Msg: err.Error()} + } +} diff --git a/controllers/payment.go b/controllers/payment.go index f568f8b8..8280cf62 100644 --- a/controllers/payment.go +++ b/controllers/payment.go @@ -114,3 +114,20 @@ func (c *ApiController) DeletePayment() { c.Data["json"] = wrapActionResponse(object.DeletePayment(&payment)) c.ServeJSON() } + +// @Title NotifyPayment +// @Tag Payment API +// @Description notify payment +// @Param body body object.Payment true "The details of the payment" +// @Success 200 {object} controllers.Response The Response object +// @router /notify-payment [post] +func (c *ApiController) NotifyPayment() { + var payment object.Payment + err := json.Unmarshal(c.Ctx.Input.RequestBody, &payment) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.NotifyPayment("111", "222")) + c.ServeJSON() +} diff --git a/controllers/product.go b/controllers/product.go index 0548fa78..27f6a04b 100644 --- a/controllers/product.go +++ b/controllers/product.go @@ -114,3 +114,24 @@ func (c *ApiController) DeleteProduct() { c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product)) c.ServeJSON() } + +// @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" +// @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") + host := c.Ctx.Request.Host + + payUrl, err := object.BuyProduct(id, providerId, host) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(payUrl) +} diff --git a/go.mod b/go.mod index 06ce162d..6274f18f 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( 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 + github.com/go-pay/gopay v1.5.72 github.com/go-sql-driver/mysql v1.5.0 github.com/golang-jwt/jwt/v4 v4.1.0 github.com/google/uuid v1.2.0 @@ -29,7 +30,7 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tealeg/xlsx v1.0.5 github.com/thanhpk/randstr v1.0.4 - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 + golang.org/x/crypto v0.0.0-20220208233918-bba287dce954 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect diff --git a/go.sum b/go.sum index e5f4fa9d..4fd3d42d 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-pay/gopay v1.5.72 h1:3zm64xMBhJBa8rXbm//q5UiGgOa4WO5XYEnU394N2Zw= +github.com/go-pay/gopay v1.5.72/go.mod h1:0qOGIJuFW7PKDOjmecwKyW0mgsVImgwB9yPJj0ilpn8= github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -379,8 +381,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220208233918-bba287dce954 h1:BkypuErRT9A9I/iljuaG3/zdMjd/J6m8tKKJQtGfSdA= +golang.org/x/crypto v0.0.0-20220208233918-bba287dce954/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/object/cert.go b/object/cert.go index 91a2322e..126f7762 100644 --- a/object/cert.go +++ b/object/cert.go @@ -33,8 +33,10 @@ type Cert struct { BitSize int `json:"bitSize"` ExpireInYears int `json:"expireInYears"` - PublicKey string `xorm:"mediumtext" json:"publicKey"` - PrivateKey string `xorm:"mediumtext" json:"privateKey"` + PublicKey string `xorm:"mediumtext" json:"publicKey"` + PrivateKey string `xorm:"mediumtext" json:"privateKey"` + AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"` + AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"` } func GetMaskedCert(cert *Cert) *Cert { diff --git a/object/payment.go b/object/payment.go index 2bdead86..f6190456 100644 --- a/object/payment.go +++ b/object/payment.go @@ -124,6 +124,23 @@ func DeletePayment(payment *Payment) bool { return affected != 0 } +func NotifyPayment(id string, state string) bool { + owner, name := util.GetOwnerAndNameFromId(id) + payment := getPayment(owner, name) + if payment == nil { + return false + } + + payment.State = state + + affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(payment) + if err != nil { + panic(err) + } + + return affected != 0 +} + func (payment *Payment) GetId() string { return fmt.Sprintf("%s/%s", payment.Owner, payment.Name) } diff --git a/object/product.go b/object/product.go index 920e4e68..f75d26d8 100644 --- a/object/product.go +++ b/object/product.go @@ -17,6 +17,7 @@ package object import ( "fmt" + "github.com/casdoor/casdoor/pp" "github.com/casdoor/casdoor/util" "xorm.io/core" ) @@ -31,7 +32,7 @@ type Product struct { Detail string `xorm:"varchar(100)" json:"detail"` Tag string `xorm:"varchar(100)" json:"tag"` Currency string `xorm:"varchar(100)" json:"currency"` - Price int `json:"price"` + Price float64 `json:"price"` Quantity int `json:"quantity"` Sold int `json:"sold"` Providers []string `xorm:"varchar(100)" json:"providers"` @@ -128,3 +129,47 @@ func DeleteProduct(product *Product) bool { func (product *Product) GetId() string { return fmt.Sprintf("%s/%s", product.Owner, product.Name) } + +func (product *Product) isValidProvider(provider *Provider) bool { + for _, providerName := range product.Providers { + if providerName == provider.Name { + return true + } + } + return false +} + +func BuyProduct(id string, providerId string, host string) (string, error) { + product := GetProduct(id) + if product == nil { + return "", fmt.Errorf("the product: %s does not exist", id) + } + + provider := getProvider(product.Owner, providerId) + if provider == nil { + return "", fmt.Errorf("the payment provider: %s does not exist", providerId) + } + + if !product.isValidProvider(provider) { + return "", fmt.Errorf("the payment provider: %s is not valid for the product: %s", providerId, id) + } + + cert := getCert(product.Owner, provider.Cert) + if cert == nil { + return "", fmt.Errorf("the cert: %s does not exist", provider.Cert) + } + + 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() + + originFrontend, originBackend := getOriginFromHost(host) + returnUrl := fmt.Sprintf("%s/payments/%s", originFrontend, paymentId) + notifyUrl := fmt.Sprintf("%s/api/notify-payment", originBackend) + + payUrl, err := pProvider.Pay(product.DisplayName, paymentId, product.Price, returnUrl, notifyUrl) + return payUrl, err +} diff --git a/object/product_test.go b/object/product_test.go new file mode 100644 index 00000000..f5670809 --- /dev/null +++ b/object/product_test.go @@ -0,0 +1,41 @@ +// 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 object + +import ( + "testing" + + "github.com/casdoor/casdoor/pp" + "github.com/casdoor/casdoor/util" +) + +func TestProvider(t *testing.T) { + InitConfig() + + 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) + + paymentId := util.GenerateTimeId() + returnUrl := "" + notifyUrl := "" + payUrl, err := pProvider.Pay(product.DisplayName, paymentId, product.Price, returnUrl, notifyUrl) + if err != nil { + panic(err) + } + + println(payUrl) +} diff --git a/object/provider.go b/object/provider.go index 98ec3354..111cefc6 100644 --- a/object/provider.go +++ b/object/provider.go @@ -35,6 +35,7 @@ type Provider struct { ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientId2 string `xorm:"varchar(100)" json:"clientId2"` ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"` + Cert string `xorm:"varchar(100)" json:"cert"` Host string `xorm:"varchar(100)" json:"host"` Port int `json:"port"` diff --git a/pp/alipay.go b/pp/alipay.go new file mode 100644 index 00000000..ef7bdbcc --- /dev/null +++ b/pp/alipay.go @@ -0,0 +1,65 @@ +// 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 ( + "context" + "fmt" + "strings" + + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/alipay" +) + +type AlipayPaymentProvider struct { + Client *alipay.Client +} + +func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider { + pp := &AlipayPaymentProvider{} + + client, err := alipay.NewClient(appId, appPrivateKey, true) + if err != nil { + panic(err) + } + + err = client.SetCertSnByContent([]byte(appPublicKey), []byte(authorityRootPublicKey), []byte(authorityPublicKey)) + if err != nil { + panic(err) + } + + pp.Client = client + return pp +} + +func (pp *AlipayPaymentProvider) Pay(productName string, paymentId 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("return_url", returnUrl) + bm.Set("notify_url", notifyUrl) + + payUrl, err := pp.Client.TradePagePay(context.Background(), bm) + if err != nil { + return "", err + } + return payUrl, nil +} diff --git a/pp/provider.go b/pp/provider.go new file mode 100644 index 00000000..66342369 --- /dev/null +++ b/pp/provider.go @@ -0,0 +1,26 @@ +// 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 + +type PaymentProvider interface { + Pay(productName string, paymentId string, price float64, returnUrl string, notifyUrl string) (string, error) +} + +func GetPaymentProvider(typ string, appId string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider { + if typ == "Alipay" { + return NewAlipayPaymentProvider(appId, appPublicKey, appPrivateKey, authorityPublicKey, authorityRootPublicKey) + } + return nil +} diff --git a/routers/router.go b/routers/router.go index 3a5c6f2b..0615d787 100644 --- a/routers/router.go +++ b/routers/router.go @@ -156,12 +156,14 @@ func initAPI() { beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct") beego.Router("/api/add-product", &controllers.ApiController{}, "POST:AddProduct") beego.Router("/api/delete-product", &controllers.ApiController{}, "POST:DeleteProduct") + beego.Router("/api/buy-product", &controllers.ApiController{}, "POST:BuyProduct") beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments") 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/send-email", &controllers.ApiController{}, "POST:SendEmail") beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms") diff --git a/util/string.go b/util/string.go index 615ec56e..c2c71656 100644 --- a/util/string.go +++ b/util/string.go @@ -23,6 +23,7 @@ import ( "os" "strconv" "strings" + "time" "unicode" "github.com/google/uuid" @@ -88,6 +89,17 @@ func GenerateId() string { return uuid.NewString() } +func GenerateTimeId() string { + timestamp := time.Now().Unix() + tm := time.Unix(timestamp, 0) + t := tm.Format("20060102_150405") + + random := uuid.NewString()[0:7] + + res := fmt.Sprintf("%s_%s", t, random) + return res +} + func GetId(name string) string { return fmt.Sprintf("admin/%s", name) } diff --git a/web/src/ProductBuyPage.js b/web/src/ProductBuyPage.js index ee8b1c91..cf8cf385 100644 --- a/web/src/ProductBuyPage.js +++ b/web/src/ProductBuyPage.js @@ -18,6 +18,7 @@ import i18next from "i18next"; import * as ProductBackend from "./backend/ProductBackend"; import * as ProviderBackend from "./backend/ProviderBackend"; import * as Provider from "./auth/Provider"; +import * as Setting from "./Setting"; class ProductBuyPage extends React.Component { constructor(props) { @@ -107,6 +108,24 @@ class ProductBuyPage extends React.Component { // } } + buyProduct(product, provider) { + ProductBackend.buyProduct(this.state.product.owner, this.state.productName, provider.name) + .then((res) => { + if (res.msg === "") { + const payUrl = res.data; + Setting.goToLink(payUrl); + this.setState({ + productName: this.state.product.name, + }); + } else { + Setting.showMessage("error", res.msg); + } + }) + .catch(error => { + Setting.showMessage("error", `Failed to connect to server: ${error}`); + }); + } + getPayButton(provider) { let text = provider.type; if (provider.type === "Alipay") { @@ -131,11 +150,11 @@ class ProductBuyPage extends React.Component { renderProviderButton(provider, product) { return ( - + this.buyProduct(product, provider)}> { this.getPayButton(provider) } - + ) } diff --git a/web/src/ProductListPage.js b/web/src/ProductListPage.js index c01564f2..1afdd6c6 100644 --- a/web/src/ProductListPage.js +++ b/web/src/ProductListPage.js @@ -225,11 +225,12 @@ class ProductListPage extends BaseListPage { title: i18next.t("general:Action"), dataIndex: '', key: 'op', - width: '170px', + width: '230px', fixed: (Setting.isMobile()) ? "false" : "right", render: (text, record, index) => { return (
+ res.json()); } + +export function buyProduct(owner, name, providerId) { + return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerId=${providerId}`, { + method: 'POST', + credentials: 'include', + }).then(res => res.json()); +}