Compare commits

...

36 Commits

Author SHA1 Message Date
Yang Luo
eacc3fae5a fix: handle more errors in downloadImage() 2023-09-15 22:53:09 +08:00
Yang Luo
ce7a2e924b feat: fix XML format issue in GenerateCasToken() 2023-09-15 22:38:04 +08:00
Yang Luo
ece060d03d feat: fix XML bug in GenerateCasToken() 2023-09-15 18:57:59 +08:00
Yang Luo
1276da4daa Check old password for normal user in SetPassword() 2023-09-15 10:21:02 +08:00
Yang Luo
616629ef99 Refactor CheckLoginPermission() code 2023-09-15 02:47:53 +08:00
Yang Luo
b633ecdcf2 Fix bug that cannot access application's public certificate for non "admin" owner 2023-09-15 00:56:40 +08:00
Yaodong Yu
a12ba7fb85 feat: allow CORS for UserInfo API in OIDC (#2313) 2023-09-13 18:11:13 +08:00
haiwu
08a0092974 feat: fix alipay payment provider (#2330)
* feat: support alipay payment provider

* feat: update notify params

* feat: update root cert

* feat: update ProviderEditPage.js

* feat: gofumpt
2023-09-13 17:30:51 +08:00
Yang Luo
bb04b10e8b Fix JSON issue in GenerateCasToken() 2023-09-13 16:45:11 +08:00
Yang Luo
ea1414dfd0 Fix typo 2023-09-13 00:19:32 +08:00
Yang Luo
32a8a028d5 Set TOTP issuer to Casdoor 2023-09-12 23:56:39 +08:00
Yang Luo
0fe34c2f53 Fix the issue that database syncer can't work with null-fields on source table 2023-09-12 16:06:44 +08:00
UsherFall
dc57c476b7 feat: support acs email provider (#2323)
* feat: support acs email provider

* feat: support acs email provider

* hide Test SMTP Connection button

* fix name acs
2023-09-12 02:13:37 +08:00
Cattī Crūdēlēs
a7cb202ee9 feat: fix JSON tag of EmailVerified (#2322)
Signed-off-by: Cattī Crūdēlēs <17695588+wzy9607@users.noreply.github.com>
2023-09-11 18:33:24 +08:00
Yang Luo
e5e264628e Remove "RUN mkdir tempFiles" 2023-09-09 20:24:18 +08:00
Palp1tate
8d4127f744 feat: improve dashboard UI for mobile devices (#2320) 2023-09-09 16:17:24 +08:00
Yang Luo
1305899060 Fix "app" user API denied issue 2023-09-09 15:44:36 +08:00
Yang Luo
411a85c7ab Remove useless GetMaxLenStr() 2023-09-09 15:40:35 +08:00
Yang Luo
f39358e122 Improve SMS Test's initial value 2023-09-09 02:38:15 +08:00
Yang Luo
a84752bbb5 Update go-sms-sender to v0.14.0 2023-09-09 02:15:38 +08:00
Baihhh
e9d8ab8cdb fix: hide tour component for mobile (#2317) 2023-09-08 22:53:46 +08:00
haiwu
d12088e8e7 feat: fix bug in pricing when signup by phone (#2316)
* fix: fix bug in pricing

* fix: remove log
2023-09-08 21:03:30 +08:00
Yang Luo
c62588f9bc Add EmailVerified to UserInfo 2023-09-08 18:27:14 +08:00
haiwu
16cd09d175 feat: support wechat pay (#2312)
* feat: support wechat pay

* feat: support wechat pay

* feat: update wechatpay.go

* feat: add router /qrcode
2023-09-07 15:45:54 +08:00
Yang Luo
7318ee6e3a Improve LocalFileSystemProvider's error handling 2023-09-07 10:49:39 +08:00
Yang Luo
3459ef1479 Improve termsOfUse UI and error handling 2023-09-07 10:33:20 +08:00
UsherFall
ca6b27f922 feat: fix notification provider frontend bug and twitter error (#2310) 2023-09-06 23:41:34 +08:00
Yang Luo
e528e8883b Add "localhost" to IsRedirectUriValid() 2023-09-06 21:14:58 +08:00
Yang Luo
b7cd604e56 Mask user in GenerateCasToken() 2023-09-06 18:36:55 +08:00
Yang Luo
3c2fd574a6 Refactor GenerateCasToken() 2023-09-06 18:35:13 +08:00
Yang Luo
a9de7d3aef Add groups to permission 2023-09-06 00:10:33 +08:00
Yang Luo
9820801634 Make Product's Providers longer (255) 2023-09-05 20:24:24 +08:00
UsherFall
c6e422c3a8 feat: add multiple notification providers (#2302)
* feat: support dingtalk notification provider

* feat: support lark notification provider

* feat: support microsoft teams notification provider

* feat: support bark notification provider

* feat: support pushover notification provider

* feat: support pushbullet notification provider

* feat: support slack notification provider

* feat: support webpush notification provider

* fix go-test error

* update notify repository

* feat: support discord notification provider

* feat: support google chat notification provider

* feat: support Line notification provider

* feat: support matrix notification provider

* feat: support twitter notification provider

* fix lint

* add no proxy provider

* update setting.js

* update social_teams
2023-09-05 17:05:34 +08:00
UsherFall
bc8e9cfd64 feat: storage provider's domain initial value bug (#2303) 2023-09-05 14:53:32 +08:00
Yang Luo
c1eae9fcd8 Fix TotpMfa's Verify() 2023-09-04 19:21:26 +08:00
YunShu
6dae6e4954 docs: fix all dead links (#2297)
https://github.com/Selflocking/linkchecker/actions/runs/6058177987
2023-09-03 21:19:23 +08:00
82 changed files with 2111 additions and 646 deletions

View File

@@ -64,7 +64,6 @@ COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf
COPY --from=BACK /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt COPY --from=BACK /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
COPY --from=FRONT /web/build ./web/build COPY --from=FRONT /web/build ./web/build
RUN mkdir tempFiles
ENTRYPOINT ["/bin/bash"] ENTRYPOINT ["/bin/bash"]
CMD ["/docker-entrypoint.sh"] CMD ["/docker-entrypoint.sh"]

View File

@@ -88,6 +88,7 @@ p, *, *, *, /api/metrics, *, *
p, *, *, GET, /api/get-pricing, *, * p, *, *, GET, /api/get-pricing, *, *
p, *, *, GET, /api/get-plan, *, * p, *, *, GET, /api/get-plan, *, *
p, *, *, GET, /api/get-subscription, *, * p, *, *, GET, /api/get-subscription, *, *
p, *, *, GET, /api/get-provider, *, *
p, *, *, GET, /api/get-organization-names, *, * p, *, *, GET, /api/get-organization-names, *, *
` `
@@ -120,6 +121,10 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
panic(err) panic(err)
} }
if subOwner == "app" {
return true
}
if user != nil && user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) { if user != nil && user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
return true return true
} }

View File

@@ -90,14 +90,24 @@ func (c *ApiController) GetApplication() {
return return
} }
if c.Input().Get("withKey") != "" && application.Cert != "" { if c.Input().Get("withKey") != "" && application != nil && application.Cert != "" {
cert, err := object.GetCert(util.GetId(application.Owner, application.Cert)) cert, err := object.GetCert(util.GetId(application.Owner, application.Cert))
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
application.CertPublicKey = cert.Certificate if cert == nil {
cert, err = object.GetCert(util.GetId(application.Organization, application.Cert))
if err != nil {
c.ResponseError(err.Error())
return
}
}
if cert != nil {
application.CertPublicKey = cert.Certificate
}
} }
c.ResponseOk(object.GetMaskedApplication(application, userId)) c.ResponseOk(object.GetMaskedApplication(application, userId))

View File

@@ -59,7 +59,7 @@ func tokenToResponse(token *object.Token) *Response {
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) { func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
userId := user.GetId() userId := user.GetId()
allowed, err := object.CheckAccessPermission(userId, application) allowed, err := object.CheckLoginPermission(userId, application)
if err != nil { if err != nil {
c.ResponseError(err.Error(), nil) c.ResponseError(err.Error(), nil)
return return

View File

@@ -176,11 +176,10 @@ func (c *ApiController) DeletePayment() {
func (c *ApiController) NotifyPayment() { func (c *ApiController) NotifyPayment() {
owner := c.Ctx.Input.Param(":owner") owner := c.Ctx.Input.Param(":owner")
paymentName := c.Ctx.Input.Param(":payment") paymentName := c.Ctx.Input.Param(":payment")
orderId := c.Ctx.Input.Param("order")
body := c.Ctx.Input.RequestBody body := c.Ctx.Input.RequestBody
payment, err := object.NotifyPayment(c.Ctx.Request, body, owner, paymentName, orderId) payment, err := object.NotifyPayment(body, owner, paymentName)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@@ -187,11 +187,11 @@ func (c *ApiController) BuyProduct() {
return return
} }
payUrl, orderId, err := object.BuyProduct(id, user, providerName, pricingName, planName, host) payment, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk(payUrl, orderId) c.ResponseOk(payment)
} }

View File

@@ -156,7 +156,7 @@ func (c *ApiController) DeleteToken() {
// @Success 200 {object} object.TokenWrapper The Response object // @Success 200 {object} object.TokenWrapper The Response object
// @Success 400 {object} object.TokenError The Response object // @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object // @Success 401 {object} object.TokenError The Response object
// @router /login/oauth/access_token [post] // @router api/login/oauth/access_token [post]
func (c *ApiController) GetOAuthToken() { func (c *ApiController) GetOAuthToken() {
grantType := c.Input().Get("grant_type") grantType := c.Input().Get("grant_type")
refreshToken := c.Input().Get("refresh_token") refreshToken := c.Input().Get("refresh_token")

View File

@@ -457,7 +457,16 @@ func (c *ApiController) SetPassword() {
return return
} }
if oldPassword != "" { isAdmin := c.IsAdmin()
if isAdmin {
if oldPassword != "" {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
return
}
}
} else {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage()) msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" { if msg != "" {
c.ResponseError(msg) c.ResponseError(msg)

227
email/azure_acs.go Normal file
View File

@@ -0,0 +1,227 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package email
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
const (
importanceNormal = "normal"
sendEmailEndpoint = "/emails:send"
apiVersion = "2023-03-31"
)
type Email struct {
Recipients Recipients `json:"recipients"`
SenderAddress string `json:"senderAddress"`
Content Content `json:"content"`
Headers []CustomHeader `json:"headers"`
Tracking bool `json:"disableUserEngagementTracking"`
Importance string `json:"importance"`
ReplyTo []EmailAddress `json:"replyTo"`
Attachments []Attachment `json:"attachments"`
}
type Recipients struct {
To []EmailAddress `json:"to"`
CC []EmailAddress `json:"cc"`
BCC []EmailAddress `json:"bcc"`
}
type EmailAddress struct {
DisplayName string `json:"displayName"`
Address string `json:"address"`
}
type Content struct {
Subject string `json:"subject"`
HTML string `json:"html"`
PlainText string `json:"plainText"`
}
type CustomHeader struct {
Name string `json:"name"`
Value string `json:"value"`
}
type Attachment struct {
Content string `json:"contentBytesBase64"`
AttachmentType string `json:"attachmentType"`
Name string `json:"name"`
}
type ErrorResponse struct {
Error CommunicationError `json:"error"`
}
// CommunicationError contains the error code and message
type CommunicationError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type AzureACSEmailProvider struct {
AccessKey string
Endpoint string
}
func NewAzureACSEmailProvider(accessKey string, endpoint string) *AzureACSEmailProvider {
return &AzureACSEmailProvider{
AccessKey: accessKey,
Endpoint: endpoint,
}
}
func newEmail(fromAddress string, toAddress string, subject string, content string) *Email {
return &Email{
Recipients: Recipients{
To: []EmailAddress{
{
DisplayName: toAddress,
Address: toAddress,
},
},
},
SenderAddress: fromAddress,
Content: Content{
Subject: subject,
HTML: content,
},
Importance: importanceNormal,
}
}
func (a *AzureACSEmailProvider) sendEmail(e *Email) error {
postBody, err := json.Marshal(e)
if err != nil {
return fmt.Errorf("email JSON marshall failed: %s", err)
}
bodyBuffer := bytes.NewBuffer(postBody)
req, err := http.NewRequest("POST", a.Endpoint+sendEmailEndpoint+"?api-version="+apiVersion, bodyBuffer)
if err != nil {
return fmt.Errorf("error creating AzureACS API request: %s", err)
}
// Sign the request using the AzureACS access key and HMAC-SHA256
err = signRequestHMAC(a.AccessKey, req)
if err != nil {
return fmt.Errorf("error signing AzureACS API request: %s", err)
}
req.Header.Set("Content-Type", "application/json")
// Some important header
req.Header.Set("repeatability-request-id", uuid.New().String())
req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat))
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error sending AzureACS API request: %s", err)
}
defer resp.Body.Close()
// Response error Handling
if resp.StatusCode == http.StatusBadRequest {
commError := ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(&commError)
if err != nil {
return err
}
return fmt.Errorf("error sending email: %s", commError.Error.Message)
}
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("error sending email: status: %d", resp.StatusCode)
}
return nil
}
func signRequestHMAC(secret string, req *http.Request) error {
method := req.Method
host := req.URL.Host
pathAndQuery := req.URL.Path
if req.URL.RawQuery != "" {
pathAndQuery = pathAndQuery + "?" + req.URL.RawQuery
}
var content []byte
var err error
if req.Body != nil {
content, err = io.ReadAll(req.Body)
if err != nil {
// return err
content = []byte{}
}
}
req.Body = io.NopCloser(bytes.NewBuffer(content))
key, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return fmt.Errorf("error decoding secret: %s", err)
}
timestamp := time.Now().UTC().Format(http.TimeFormat)
contentHash := GetContentHashBase64(content)
stringToSign := fmt.Sprintf("%s\n%s\n%s;%s;%s", strings.ToUpper(method), pathAndQuery, timestamp, host, contentHash)
signature := GetHmac(stringToSign, key)
req.Header.Set("x-ms-content-sha256", contentHash)
req.Header.Set("x-ms-date", timestamp)
req.Header.Set("Authorization", "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature="+signature)
return nil
}
func GetContentHashBase64(content []byte) string {
hasher := sha256.New()
hasher.Write(content)
return base64.StdEncoding.EncodeToString(hasher.Sum(nil))
}
func GetHmac(content string, key []byte) string {
hmac := hmac.New(sha256.New, key)
hmac.Write([]byte(content))
return base64.StdEncoding.EncodeToString(hmac.Sum(nil))
}
func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
e := newEmail(fromAddress, toAddress, subject, content)
return a.sendEmail(e)
}

27
email/provider.go Normal file
View File

@@ -0,0 +1,27 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package email
type EmailProvider interface {
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
}
func GetEmailProvider(typ string, clientId string, clientSecret string, appId string, host string, port int, disableSsl bool) EmailProvider {
if typ == "Azure ACS" {
return NewAzureACSEmailProvider(appId, host)
} else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
}
}

49
email/smtp.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package email
import (
"crypto/tls"
"github.com/casdoor/gomail/v2"
)
type SmtpEmailProvider struct {
Dialer *gomail.Dialer
}
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool) *SmtpEmailProvider {
dialer := &gomail.Dialer{}
dialer = gomail.NewDialer(host, port, userName, password)
if typ == "SUBMAIL" {
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
dialer.SSL = !disableSsl
return &SmtpEmailProvider{Dialer: dialer}
}
func (s *SmtpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
message := gomail.NewMessage()
message.SetAddressHeader("From", fromAddress, fromName)
message.SetHeader("To", toAddress)
message.SetHeader("Subject", subject)
message.SetBody("text/html", content)
message.SkipUsernameCheck = true
return s.Dialer.DialAndSend(message)
}

19
go.mod
View File

@@ -6,14 +6,14 @@ require (
github.com/Masterminds/squirrel v1.5.3 github.com/Masterminds/squirrel v1.5.3
github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aliyun/alibaba-cloud-sdk-go v1.62.188 // indirect github.com/aws/aws-sdk-go v1.45.5
github.com/aws/aws-sdk-go v1.44.4
github.com/beego/beego v1.12.12 github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/casbin/casbin v1.9.1 // indirect github.com/casbin/casbin v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.30.1 github.com/casbin/casbin/v2 v2.37.0
github.com/casdoor/go-sms-sender v0.12.0 github.com/casdoor/go-sms-sender v0.14.0
github.com/casdoor/gomail/v2 v2.0.1 github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.43.0
github.com/casdoor/oss v1.3.0 github.com/casdoor/oss v1.3.0
github.com/casdoor/xorm-adapter/v3 v3.0.4 github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/casvisor/casvisor-go-sdk v1.0.3 github.com/casvisor/casvisor-go-sdk v1.0.3
@@ -30,14 +30,13 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.6.0 github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.1
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lestrrat-go/jwx v1.2.21 github.com/lestrrat-go/jwx v1.2.21
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
github.com/markbates/goth v1.75.2 github.com/markbates/goth v1.75.2
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/nikoksr/notify v0.41.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5 github.com/nyaruka/phonenumbers v1.1.5
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
@@ -60,10 +59,12 @@ require (
github.com/xorm-io/core v0.7.4 github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6 github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.11.0 golang.org/x/crypto v0.12.0
golang.org/x/net v0.13.0 golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.10.0 golang.org/x/oauth2 v0.11.0
google.golang.org/api v0.138.0
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0
maunium.net/go/mautrix v0.16.0
modernc.org/sqlite v1.18.2 modernc.org/sqlite v1.18.2
) )

392
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -72,13 +72,13 @@ type FacebookCheckToken struct {
} }
// FacebookCheckTokenData // FacebookCheckTokenData
// Get more detail via: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#checktoken // Get more detail via: https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#checktoken
type FacebookCheckTokenData struct { type FacebookCheckTokenData struct {
UserId string `json:"user_id"` UserId string `json:"user_id"`
} }
// GetToken use code get access_token (*operation of getting code ought to be done in front) // GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#confirm // get more detail via: https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#confirm
func (idp *FacebookIdProvider) GetToken(code string) (*oauth2.Token, error) { func (idp *FacebookIdProvider) GetToken(code string) (*oauth2.Token, error) {
params := url.Values{} params := url.Values{}
params.Add("client_id", idp.Config.ClientID) params.Add("client_id", idp.Config.ClientID)

29
notification/bark.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/bark"
)
func NewBarkProvider(deviceKey string) (notify.Notifier, error) {
barkSrv := bark.New(deviceKey)
notifier := notify.New()
notifier.UseServices(barkSrv)
return notifier, nil
}

33
notification/dingtalk.go Normal file
View File

@@ -0,0 +1,33 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/dingding"
)
func NewDingTalkProvider(token string, secret string) (notify.Notifier, error) {
cfg := dingding.Config{
Token: token,
Secret: secret,
}
dingtalkSrv := dingding.New(&cfg)
notifier := notify.New()
notifier.UseServices(dingtalkSrv)
return notifier, nil
}

37
notification/discord.go Normal file
View File

@@ -0,0 +1,37 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/discord"
)
func NewDiscordProvider(token string, channelId string) (*notify.Notify, error) {
discordSrv := discord.New()
err := discordSrv.AuthenticateWithBotToken(token)
if err != nil {
return nil, err
}
discordSrv.SetHttpClient(proxy.ProxyHttpClient)
discordSrv.AddReceivers(channelId)
notifier := notify.NewWithServices(discordSrv)
return notifier, nil
}

View File

@@ -0,0 +1,53 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"context"
"strings"
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/googlechat"
"google.golang.org/api/chat/v1"
"google.golang.org/api/option"
)
func NewGoogleChatProvider(credentials string) (*notify.Notify, error) {
withCred := option.WithCredentialsJSON([]byte(credentials))
withSpacesScope := option.WithScopes("https://www.googleapis.com/auth/chat.spaces")
listSvc, err := chat.NewService(context.Background(), withCred, withSpacesScope)
spaces, err := listSvc.Spaces.List().Do()
if err != nil {
return nil, err
}
receivers := make([]string, 0)
for _, space := range spaces.Spaces {
name := strings.Replace(space.Name, "spaces/", "", 1)
receivers = append(receivers, name)
}
googleChatSrv, err := googlechat.New(withCred)
if err != nil {
return nil, err
}
googleChatSrv.AddReceivers(receivers...)
notifier := notify.NewWithServices(googleChatSrv)
return notifier, nil
}

29
notification/lark.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/lark"
)
func NewLarkProvider(webhookURL string) (notify.Notifier, error) {
larkSrv := lark.NewWebhookService(webhookURL)
notifier := notify.New()
notifier.UseServices(larkSrv)
return notifier, nil
}

32
notification/line.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/line"
)
func NewLineProvider(channelSecret string, accessToken string, receiver string) (*notify.Notify, error) {
lineSrv, _ := line.NewWithHttpClient(channelSecret, accessToken, proxy.ProxyHttpClient)
lineSrv.AddReceivers(receiver)
notifier := notify.New()
notifier.UseServices(lineSrv)
return notifier, nil
}

36
notification/matrix.go Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/matrix"
"maunium.net/go/mautrix/id"
)
func NewMatrixProvider(userId string, roomId string, accessToken string, homeServer string) (*notify.Notify, error) {
matrixSrv, err := matrix.New(id.UserID(userId), id.RoomID(roomId), homeServer, accessToken)
if err != nil {
return nil, err
}
matrixSrv.SetHttpClient(proxy.ProxyHttpClient)
notifier := notify.New()
notifier.UseServices(matrixSrv)
return notifier, nil
}

View File

@@ -0,0 +1,31 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/msteams"
)
func NewMicrosoftTeamsProvider(webhookURL string) (notify.Notifier, error) {
msTeamsSrv := msteams.New()
msTeamsSrv.AddReceivers(webhookURL)
notifier := notify.New()
notifier.UseServices(msTeamsSrv)
return notifier, nil
}

View File

@@ -14,13 +14,45 @@
package notification package notification
import "github.com/nikoksr/notify" import "github.com/casdoor/notify"
func GetNotificationProvider(typ string, appId string, receiver string, method string, title string) (notify.Notifier, error) { func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string) (notify.Notifier, error) {
if typ == "Telegram" { if typ == "Telegram" {
return NewTelegramProvider(appId, receiver) return NewTelegramProvider(appId, receiver)
} else if typ == "Custom HTTP" { } else if typ == "Custom HTTP" {
return NewCustomHttpProvider(receiver, method, title) return NewCustomHttpProvider(receiver, method, title)
} else if typ == "DingTalk" {
return NewDingTalkProvider(appId, receiver)
} else if typ == "Lark" {
return NewLarkProvider(receiver)
} else if typ == "Microsoft Teams" {
return NewMicrosoftTeamsProvider(receiver)
} else if typ == "Bark" {
return NewBarkProvider(receiver)
} else if typ == "Pushover" {
return NewPushoverProvider(appId, receiver)
} else if typ == "Pushbullet" {
return NewPushbulletProvider(appId, receiver)
} else if typ == "Slack" {
return NewSlackProvider(appId, receiver)
} else if typ == "Webpush" {
return NewWebpushProvider(clientId, clientSecret, receiver)
} else if typ == "Discord" {
return NewDiscordProvider(appId, receiver)
} else if typ == "Google Chat" {
return NewGoogleChatProvider(metaData)
} else if typ == "Line" {
return NewLineProvider(clientSecret, appId, receiver)
} else if typ == "Matrix" {
return NewMatrixProvider(clientId, clientSecret, appId, receiver)
} else if typ == "Twitter" {
return NewTwitterProvider(clientId, clientSecret, clientId2, clientSecret2, receiver)
} else if typ == "Reddit" {
return NewRedditProvider(clientId, clientSecret, clientId2, clientSecret2, receiver)
} else if typ == "Rocket Chat" {
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
} else if typ == "Viber" {
return NewViberProvider(clientId, clientSecret, appId, receiver)
} }
return nil, nil return nil, nil

View File

@@ -0,0 +1,31 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/pushbullet"
)
func NewPushbulletProvider(apiToken string, deviceNickname string) (notify.Notifier, error) {
pushbulletSrv := pushbullet.New(apiToken)
pushbulletSrv.AddReceivers(deviceNickname)
notifier := notify.New()
notifier.UseServices(pushbulletSrv)
return notifier, nil
}

31
notification/pushover.go Normal file
View File

@@ -0,0 +1,31 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/pushover"
)
func NewPushoverProvider(appToken string, recipientID string) (notify.Notifier, error) {
pushoverSrv := pushover.New(appToken)
pushoverSrv.AddReceivers(recipientID)
notifier := notify.New()
notifier.UseServices(pushoverSrv)
return notifier, nil
}

34
notification/reddit.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/reddit"
)
func NewRedditProvider(clientId string, clientSecret string, username string, password string, recipient string) (notify.Notifier, error) {
redditSrv, err := reddit.New(clientId, clientSecret, username, password)
if err != nil {
return nil, err
}
redditSrv.AddReceivers(recipient)
notifier := notify.New()
notifier.UseServices(redditSrv)
return notifier, nil
}

View File

@@ -0,0 +1,47 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"fmt"
"strings"
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/rocketchat"
)
func NewRocketChatProvider(clientId string, clientSecret string, endpoint string, channelName string) (notify.Notifier, error) {
parts := strings.Split(endpoint, "://")
var scheme, serverURL string
if len(parts) >= 2 {
scheme = parts[0]
serverURL = parts[1]
} else {
return nil, fmt.Errorf("parse endpoint error")
}
rocketChatSrv, err := rocketchat.New(serverURL, scheme, clientId, clientSecret)
if err != nil {
return nil, err
}
rocketChatSrv.AddReceivers(channelName)
notifier := notify.New()
notifier.UseServices(rocketChatSrv)
return notifier, nil
}

30
notification/slack.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/slack"
)
func NewSlackProvider(apiToken string, channelID string) (*notify.Notify, error) {
slackSrv := slack.New(apiToken)
slackSrv.AddReceivers(channelID)
notifier := notify.New()
notifier.UseServices(slackSrv)
return notifier, nil
}

View File

@@ -18,9 +18,9 @@ import (
"strconv" "strconv"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/telegram"
api "github.com/go-telegram-bot-api/telegram-bot-api" api "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/telegram"
) )
func NewTelegramProvider(apiToken string, chatIdStr string) (notify.Notifier, error) { func NewTelegramProvider(apiToken string, chatIdStr string) (notify.Notifier, error) {
@@ -28,15 +28,18 @@ func NewTelegramProvider(apiToken string, chatIdStr string) (notify.Notifier, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
t := &telegram.Telegram{} telegramSrv := &telegram.Telegram{}
t.SetClient(client) telegramSrv.SetClient(client)
chatId, err := strconv.ParseInt(chatIdStr, 10, 64) chatId, err := strconv.ParseInt(chatIdStr, 10, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
t.AddReceivers(chatId) telegramSrv.AddReceivers(chatId)
return t, nil notifier := notify.New()
notifier.UseServices(telegramSrv)
return notifier, nil
} }

41
notification/twitter.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/twitter"
)
func NewTwitterProvider(consumerKey string, consumerSecret string, accessToken string, accessTokenSecret string, twitterId string) (*notify.Notify, error) {
credentials := twitter.Credentials{
ConsumerKey: consumerKey,
ConsumerSecret: consumerSecret,
AccessToken: accessToken,
AccessTokenSecret: accessTokenSecret,
}
twitterSrv, err := twitter.NewWithHttpClient(credentials, proxy.ProxyHttpClient)
if err != nil {
return nil, err
}
twitterSrv.AddReceivers(twitterId)
notifier := notify.New()
notifier.UseServices(twitterSrv)
return notifier, nil
}

36
notification/viber.go Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/viber"
)
func NewViberProvider(senderName string, appKey string, webhookURL string, receiverId string) (notify.Notifier, error) {
viberSrv := viber.New(appKey, senderName, "")
err := viberSrv.SetWebhook(webhookURL)
if err != nil {
return nil, err
}
viberSrv.AddReceivers(receiverId)
notifier := notify.New()
notifier.UseServices(viberSrv)
return notifier, nil
}

33
notification/webpush.go Normal file
View File

@@ -0,0 +1,33 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notification
import (
"github.com/casdoor/notify"
"github.com/casdoor/notify/service/webpush"
)
func NewWebpushProvider(publicKey string, privateKey string, endpoint string) (*notify.Notify, error) {
webpushSrv := webpush.New(publicKey, privateKey)
subscription := webpush.Subscription{
Endpoint: endpoint,
}
webpushSrv.AddReceivers(subscription)
notifier := notify.NewWithServices(webpushSrv)
return notifier, nil
}

View File

@@ -428,15 +428,14 @@ func (application *Application) GetId() string {
} }
func (application *Application) IsRedirectUriValid(redirectUri string) bool { func (application *Application) IsRedirectUriValid(redirectUri string) bool {
isValid := false redirectUris := append([]string{"http://localhost:"}, application.RedirectUris...)
for _, targetUri := range application.RedirectUris { for _, targetUri := range redirectUris {
targetUriRegex := regexp.MustCompile(targetUri) targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) { if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
isValid = true return true
break
} }
} }
return isValid return false
} }
func IsOriginAllowed(origin string) (bool, error) { func IsOriginAllowed(origin string) (bool, error) {

View File

@@ -33,10 +33,8 @@ type Cert struct {
BitSize int `json:"bitSize"` BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"` ExpireInYears int `json:"expireInYears"`
Certificate string `xorm:"mediumtext" json:"certificate"` Certificate string `xorm:"mediumtext" json:"certificate"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"` PrivateKey string `xorm:"mediumtext" json:"privateKey"`
AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"`
AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"`
} }
func GetMaskedCert(cert *Cert) *Cert { func GetMaskedCert(cert *Cert) *Cert {

BIN
object/cert.go~ Normal file

Binary file not shown.

View File

@@ -350,7 +350,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
return hasPermission, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation")) return hasPermission, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
} }
func CheckAccessPermission(userId string, application *Application) (bool, error) { func CheckLoginPermission(userId string, application *Application) (bool, error) {
var err error var err error
if userId == "built-in/admin" { if userId == "built-in/admin" {
return true, nil return true, nil
@@ -361,32 +361,40 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
return false, err return false, err
} }
allowed := true allowCount := 0
denyCount := 0
for _, permission := range permissions { for _, permission := range permissions {
if !permission.IsEnabled { if !permission.IsEnabled || permission.ResourceType != "Application" || !permission.isResourceHit(application.Name) {
continue continue
} }
isHit := false if permission.isUserHit(userId) {
for _, resource := range permission.Resources { allowCount += 1
if application.Name == resource {
isHit = true
break
}
} }
if isHit { enforcer := getPermissionEnforcer(permission)
containsAsterisk := ContainsAsterisk(userId, permission.Users)
if containsAsterisk { var isAllowed bool
return true, err isAllowed, err = enforcer.Enforce(userId, application.Name, "Read")
if err != nil {
return false, err
}
if isAllowed {
if permission.Effect == "Allow" {
allowCount += 1
} }
enforcer := getPermissionEnforcer(permission) } else {
if allowed, err = enforcer.Enforce(userId, application.Name, "read"); allowed { if permission.Effect == "Deny" {
return allowed, err denyCount += 1
} }
} }
} }
return allowed, err
if denyCount > 0 {
return false, nil
}
return true, nil
} }
func CheckUsername(username string, lang string) string { func CheckUsername(username string, lang string) string {

View File

@@ -19,6 +19,7 @@ package object
import ( import (
"crypto/tls" "crypto/tls"
"github.com/casdoor/casdoor/email"
"github.com/casdoor/gomail/v2" "github.com/casdoor/gomail/v2"
) )
@@ -35,9 +36,7 @@ func getDialer(provider *Provider) *gomail.Dialer {
} }
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error { func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
dialer := getDialer(provider) emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.AppId, provider.Host, provider.Port, provider.DisableSsl)
message := gomail.NewMessage()
fromAddress := provider.ClientId2 fromAddress := provider.ClientId2
if fromAddress == "" { if fromAddress == "" {
@@ -49,14 +48,7 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
fromName = sender fromName = sender
} }
message.SetAddressHeader("From", fromAddress, fromName) return emailProvider.Send(fromAddress, fromName, dest, title, content)
message.SetHeader("To", dest)
message.SetHeader("Subject", title)
message.SetBody("text/html", content)
message.SkipUsernameCheck = true
return dialer.DialAndSend(message)
} }
// DailSmtpServer Dail Smtp server // DailSmtpServer Dail Smtp server

View File

@@ -19,7 +19,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/beego/beego"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pquerna/otp" "github.com/pquerna/otp"
@@ -39,10 +38,11 @@ type TotpMfa struct {
} }
func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) { func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
issuer := beego.AppConfig.String("appname") //issuer := beego.AppConfig.String("appname")
if issuer == "" { //if issuer == "" {
issuer = "casdoor" // issuer = "casdoor"
} //}
issuer := "casdoor"
key, err := totp.Generate(totp.GenerateOpts{ key, err := totp.Generate(totp.GenerateOpts{
Issuer: issuer, Issuer: issuer,
@@ -81,12 +81,15 @@ func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
return errors.New("totp secret is missing") return errors.New("totp secret is missing")
} }
result, _ := totp.ValidateCustom(passcode, secret.(string), time.Now().UTC(), totp.ValidateOpts{ result, err := totp.ValidateCustom(passcode, secret.(string), time.Now().UTC(), totp.ValidateOpts{
Period: MfaTotpPeriodInSeconds, Period: MfaTotpPeriodInSeconds,
Skew: 1, Skew: 1,
Digits: otp.DigitsSix, Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1, Algorithm: otp.AlgorithmSHA1,
}) })
if err != nil {
return err
}
if result { if result {
return nil return nil
@@ -125,7 +128,15 @@ func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
} }
func (mfa *TotpMfa) Verify(passcode string) error { func (mfa *TotpMfa) Verify(passcode string) error {
result := totp.Validate(passcode, mfa.Config.Secret) result, err := totp.ValidateCustom(passcode, mfa.Config.Secret, time.Now().UTC(), totp.ValidateOpts{
Period: MfaTotpPeriodInSeconds,
Skew: 1,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
if err != nil {
return err
}
if result { if result {
return nil return nil

View File

@@ -18,12 +18,12 @@ import (
"context" "context"
"github.com/casdoor/casdoor/notification" "github.com/casdoor/casdoor/notification"
"github.com/nikoksr/notify" "github.com/casdoor/notify"
) )
func getNotificationClient(provider *Provider) (notify.Notifier, error) { func getNotificationClient(provider *Provider) (notify.Notifier, error) {
var client notify.Notifier var client notify.Notifier
client, err := notification.GetNotificationProvider(provider.Type, provider.AppId, provider.Receiver, provider.Method, provider.Title) client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -16,7 +16,6 @@ package object
import ( import (
"fmt" "fmt"
"net/http"
"github.com/casdoor/casdoor/pp" "github.com/casdoor/casdoor/pp"
@@ -55,6 +54,7 @@ type Payment struct {
// Order Info // Order Info
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"` OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"` PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl""` // `successUrl` is redirected from `payUrl` after pay success
State pp.PaymentState `xorm:"varchar(100)" json:"state"` State pp.PaymentState `xorm:"varchar(100)" json:"state"`
Message string `xorm:"varchar(2000)" json:"message"` Message string `xorm:"varchar(2000)" json:"message"`
} }
@@ -152,7 +152,7 @@ func DeletePayment(payment *Payment) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func notifyPayment(request *http.Request, body []byte, owner string, paymentName string, orderId string) (*Payment, *pp.NotifyResult, error) { func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp.NotifyResult, error) {
payment, err := getPayment(owner, paymentName) payment, err := getPayment(owner, paymentName)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -166,7 +166,7 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
pProvider, cert, err := provider.getPaymentProvider() pProvider, err := GetPaymentProvider(provider)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -180,11 +180,7 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
return nil, nil, err return nil, nil, err
} }
if orderId == "" { notifyResult, err := pProvider.Notify(body, payment.OutOrderId)
orderId = payment.OutOrderId
}
notifyResult, err := pProvider.Notify(request, body, cert.AuthorityPublicKey, orderId)
if err != nil { if err != nil {
return payment, nil, err return payment, nil, err
} }
@@ -205,8 +201,8 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
return payment, notifyResult, nil return payment, notifyResult, nil
} }
func NotifyPayment(request *http.Request, body []byte, owner string, paymentName string, orderId string) (*Payment, error) { func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, error) {
payment, notifyResult, err := notifyPayment(request, body, owner, paymentName, orderId) payment, notifyResult, err := notifyPayment(body, owner, paymentName)
if payment != nil { if payment != nil {
if err != nil { if err != nil {
payment.State = pp.PaymentStateError payment.State = pp.PaymentStateError
@@ -234,7 +230,7 @@ func invoicePayment(payment *Payment) (string, error) {
return "", fmt.Errorf("the payment provider: %s does not exist", payment.Provider) return "", fmt.Errorf("the payment provider: %s does not exist", payment.Provider)
} }
pProvider, _, err := provider.getPaymentProvider() pProvider, err := GetPaymentProvider(provider)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -30,6 +30,7 @@ type Permission struct {
Description string `xorm:"varchar(100)" json:"description"` Description string `xorm:"varchar(100)" json:"description"`
Users []string `xorm:"mediumtext" json:"users"` Users []string `xorm:"mediumtext" json:"users"`
Groups []string `xorm:"mediumtext" json:"groups"`
Roles []string `xorm:"mediumtext" json:"roles"` Roles []string `xorm:"mediumtext" json:"roles"`
Domains []string `xorm:"mediumtext" json:"domains"` Domains []string `xorm:"mediumtext" json:"domains"`
@@ -60,10 +61,6 @@ type PermissionRule struct {
const builtInAvailableField = 5 // Casdoor built-in adapter, use V5 to filter permission, so has 5 available field const builtInAvailableField = 5 // Casdoor built-in adapter, use V5 to filter permission, so has 5 available field
func (p *Permission) GetId() string {
return util.GetId(p.Owner, p.Name)
}
func GetPermissionCount(owner, field, value string) (int64, error) { func GetPermissionCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Permission{}) return session.Count(&Permission{})
@@ -345,20 +342,6 @@ func GetPermissionsByModel(owner string, model string) ([]*Permission, error) {
return permissions, nil return permissions, nil
} }
func ContainsAsterisk(userId string, users []string) bool {
containsAsterisk := false
group, _ := util.GetOwnerAndNameFromId(userId)
for _, user := range users {
permissionGroup, permissionUserName := util.GetOwnerAndNameFromId(user)
if permissionGroup == group && permissionUserName == "*" {
containsAsterisk = true
break
}
}
return containsAsterisk
}
func GetMaskedPermissions(permissions []*Permission) []*Permission { func GetMaskedPermissions(permissions []*Permission) []*Permission {
for _, permission := range permissions { for _, permission := range permissions {
permission.Users = nil permission.Users = nil
@@ -388,3 +371,27 @@ func GroupPermissionsByModelAdapter(permissions []*Permission) map[string][]stri
return m return m
} }
func (p *Permission) GetId() string {
return util.GetId(p.Owner, p.Name)
}
func (p *Permission) isUserHit(name string) bool {
targetOrg, _ := util.GetOwnerAndNameFromId(name)
for _, user := range p.Users {
userOrg, userName := util.GetOwnerAndNameFromId(user)
if userOrg == targetOrg && userName == "*" {
return true
}
}
return false
}
func (p *Permission) isResourceHit(name string) bool {
for _, resource := range p.Resources {
if name == resource {
return true
}
}
return false
}

View File

@@ -37,7 +37,7 @@ type Product struct {
Price float64 `json:"price"` Price float64 `json:"price"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
Sold int `json:"sold"` Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"` Providers []string `xorm:"varchar(255)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"` State string `xorm:"varchar(100)" json:"state"`
@@ -158,24 +158,23 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
return provider, nil return provider, nil
} }
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (string, string, error) { func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (*Payment, error) {
product, err := GetProduct(id) product, err := GetProduct(id)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
if product == nil { if product == nil {
return "", "", fmt.Errorf("the product: %s does not exist", id) return nil, fmt.Errorf("the product: %s does not exist", id)
} }
provider, err := product.getProvider(providerName) provider, err := product.getProvider(providerName)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
pProvider, _, err := provider.getPaymentProvider() pProvider, err := GetPaymentProvider(provider)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
owner := product.Owner owner := product.Owner
@@ -192,15 +191,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if pricingName != "" && planName != "" { if pricingName != "" && planName != "" {
plan, err := GetPlan(util.GetId(owner, planName)) plan, err := GetPlan(util.GetId(owner, planName))
if err != nil { if err != nil {
return "", "", err return nil, err
} }
if plan == nil { if plan == nil {
return "", "", fmt.Errorf("the plan: %s does not exist", planName) return nil, fmt.Errorf("the plan: %s does not exist", planName)
} }
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period) sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
_, err = AddSubscription(sub) _, err = AddSubscription(sub)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name) returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
} }
@@ -208,10 +207,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
// Create an OrderId and get the payUrl // Create an OrderId and get the payUrl
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl) payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
// Create a Payment linked with Product and Order // Create a Payment linked with Product and Order
payment := Payment{ payment := &Payment{
Owner: product.Owner, Owner: product.Owner,
Name: paymentName, Name: paymentName,
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
@@ -230,6 +229,7 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
User: user.Name, User: user.Name,
PayUrl: payUrl, PayUrl: payUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated, State: pp.PaymentStateCreated,
OutOrderId: orderId, OutOrderId: orderId,
} }
@@ -238,15 +238,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
payment.State = pp.PaymentStatePaid payment.State = pp.PaymentStatePaid
} }
affected, err := AddPayment(&payment) affected, err := AddPayment(payment)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
if !affected { if !affected {
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment)) return nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
} }
return payUrl, orderId, err return payment, err
} }
func ExtendProductWithProviders(product *Product) error { func ExtendProductWithProviders(product *Product) error {

View File

@@ -17,31 +17,24 @@
package object package object
import ( //func TestProduct(t *testing.T) {
"testing" // InitConfig()
//
"github.com/casdoor/casdoor/pp" // product, _ := GetProduct("admin/product_123")
"github.com/casdoor/casdoor/util" // provider, _ := getProvider(product.Owner, "provider_pay_alipay")
) // cert, _ := getCert(product.Owner, "cert-pay-alipay")
// pProvider, err := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, provider.ClientId2)
func TestProduct(t *testing.T) { // if err != nil {
InitConfig() // panic(err)
// }
product, _ := GetProduct("admin/product_123") //
provider, _ := getProvider(product.Owner, "provider_pay_alipay") // paymentName := util.GenerateTimeId()
cert, _ := getCert(product.Owner, "cert-pay-alipay") // returnUrl := ""
pProvider, err := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, provider.ClientId2) // notifyUrl := ""
if err != nil { // payUrl, _, err := pProvider.Pay(provider.Name, product.Name, "alice", paymentName, product.DisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
panic(err) // if err != nil {
} // panic(err)
// }
paymentName := util.GenerateTimeId() //
returnUrl := "" // println(payUrl)
notifyUrl := "" //}
payUrl, _, err := pProvider.Pay(provider.Name, product.Name, "alice", paymentName, product.DisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil {
panic(err)
}
println(payUrl)
}

View File

@@ -251,30 +251,69 @@ func DeleteProvider(provider *Provider) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) { func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
cert := &Cert{} cert := &Cert{}
if p.Cert != "" { if p.Cert != "" {
var err error var err error
cert, err = getCert(p.Owner, p.Cert) cert, err = GetCert(util.GetId(p.Owner, p.Cert))
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
if cert == nil { if cert == nil {
return nil, nil, fmt.Errorf("the cert: %s does not exist", p.Cert) return nil, fmt.Errorf("the cert: %s does not exist", p.Cert)
} }
} }
typ := p.Type
pProvider, err := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, p.ClientId2) if typ == "Dummy" {
if err != nil { pp, err := pp.NewDummyPaymentProvider()
return nil, cert, err if err != nil {
return nil, err
}
return pp, nil
} else if typ == "Alipay" {
if p.Metadata != "" {
// alipay provider store rootCert's name in metadata
rootCert, err := GetCert(util.GetId(p.Owner, p.Metadata))
if err != nil {
return nil, err
}
if rootCert == nil {
return nil, fmt.Errorf("the cert: %s does not exist", p.Metadata)
}
pp, err := pp.NewAlipayPaymentProvider(p.ClientId, cert.Certificate, cert.PrivateKey, rootCert.Certificate, rootCert.PrivateKey)
if err != nil {
return nil, err
}
return pp, nil
} else {
return nil, fmt.Errorf("the metadata of alipay provider is empty")
}
} else if typ == "GC" {
return pp.NewGcPaymentProvider(p.ClientId, p.ClientSecret, p.Host), nil
} else if typ == "WeChat Pay" {
pp, err := pp.NewWechatPaymentProvider(p.ClientId, p.ClientSecret, p.ClientId2, cert.Certificate, cert.PrivateKey)
if err != nil {
return nil, err
}
return pp, nil
} else if typ == "PayPal" {
pp, err := pp.NewPaypalPaymentProvider(p.ClientId, p.ClientSecret)
if err != nil {
return nil, err
}
return pp, nil
} else if typ == "Stripe" {
pp, err := pp.NewStripePaymentProvider(p.ClientId, p.ClientSecret)
if err != nil {
return nil, err
}
return pp, nil
} else {
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
} }
if pProvider == nil { return nil, 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 { func (p *Provider) GetId() string {

View File

@@ -15,6 +15,7 @@
package object package object
import ( import (
"database/sql"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -31,7 +32,7 @@ type Credential struct {
} }
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) { func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
var results []map[string]string var results []map[string]sql.NullString
err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results) err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -15,6 +15,7 @@
package object package object
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
@@ -196,7 +197,7 @@ func (syncer *Syncer) getUserValue(user *User, key string) string {
} }
} }
func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*OriginalUser { func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]sql.NullString) []*OriginalUser {
users := []*OriginalUser{} users := []*OriginalUser{}
for _, result := range results { for _, result := range results {
originalUser := &OriginalUser{ originalUser := &OriginalUser{
@@ -216,11 +217,11 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
names := strings.Split(tableColumnName, "+") names := strings.Split(tableColumnName, "+")
var values []string var values []string
for _, name := range names { for _, name := range names {
values = append(values, result[strings.Trim(name, " ")]) values = append(values, result[strings.Trim(name, " ")].String)
} }
value = strings.Join(values, " ") value = strings.Join(values, " ")
} else { } else {
value = result[tableColumnName] value = result[tableColumnName].String
} }
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value) syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value)
} }
@@ -249,9 +250,9 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
// enable // enable
value, ok := result["ENABLED"] value, ok := result["ENABLED"]
if ok { if ok {
originalUser.IsForbidden = !util.ParseBool(value) originalUser.IsForbidden = !util.ParseBool(value.String)
} else { } else {
originalUser.IsForbidden = !util.ParseBool(result["enabled"]) originalUser.IsForbidden = !util.ParseBool(result["enabled"].String)
} }
} }

View File

@@ -185,38 +185,57 @@ func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService,
} }
func GenerateCasToken(userId string, service string) (string, error) { func GenerateCasToken(userId string, service string) (string, error) {
if user, err := GetUser(userId); err != nil { user, err := GetUser(userId)
if err != nil {
return "", err return "", err
} else if user != nil {
authenticationSuccess := CasAuthenticationSuccess{
User: user.Name,
Attributes: &CasAttributes{
AuthenticationDate: time.Now(),
UserAttributes: &CasUserAttributes{},
},
ProxyGrantingTicket: fmt.Sprintf("PGTIOU-%s", util.GenerateId()),
}
data, _ := json.Marshal(user)
tmp := map[string]string{}
json.Unmarshal(data, &tmp)
for k, v := range tmp {
if v != "" {
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
Name: k,
Value: v,
})
}
}
st := fmt.Sprintf("ST-%d", rand.Int())
stToServiceResponse.Store(st, &CasAuthenticationSuccessWrapper{
AuthenticationSuccess: &authenticationSuccess,
Service: service,
UserId: userId,
})
return st, nil
} else {
return "", fmt.Errorf("invalid user Id")
} }
if user == nil {
return "", fmt.Errorf("The user: %s doesn't exist", userId)
}
user, _ = GetMaskedUser(user, false)
authenticationSuccess := CasAuthenticationSuccess{
User: user.Name,
Attributes: &CasAttributes{
AuthenticationDate: time.Now(),
UserAttributes: &CasUserAttributes{},
},
ProxyGrantingTicket: fmt.Sprintf("PGTIOU-%s", util.GenerateId()),
}
data, err := json.Marshal(user)
if err != nil {
return "", err
}
tmp := map[string]interface{}{}
err = json.Unmarshal(data, &tmp)
if err != nil {
return "", err
}
for k, v := range tmp {
value := fmt.Sprintf("%v", v)
if value == "<nil>" || value == "[]" || value == "map[]" {
value = ""
}
if value != "" {
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
Name: k,
Value: value,
})
}
}
st := fmt.Sprintf("ST-%d", rand.Int())
stToServiceResponse.Store(st, &CasAuthenticationSuccessWrapper{
AuthenticationSuccess: &authenticationSuccess,
Service: service,
UserId: userId,
})
return st, nil
} }
// GetValidationBySaml // GetValidationBySaml

View File

@@ -194,16 +194,17 @@ type User struct {
} }
type Userinfo struct { type Userinfo struct {
Sub string `json:"sub"` Sub string `json:"sub"`
Iss string `json:"iss"` Iss string `json:"iss"`
Aud string `json:"aud"` Aud string `json:"aud"`
Name string `json:"preferred_username,omitempty"` Name string `json:"preferred_username,omitempty"`
DisplayName string `json:"name,omitempty"` DisplayName string `json:"name,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Avatar string `json:"picture,omitempty"` EmailVerified bool `json:"email_verified,omitempty"`
Address string `json:"address,omitempty"` Avatar string `json:"picture,omitempty"`
Phone string `json:"phone,omitempty"` Address string `json:"address,omitempty"`
Groups []string `json:"groups,omitempty"` Phone string `json:"phone,omitempty"`
Groups []string `json:"groups,omitempty"`
} }
type ManagedAccount struct { type ManagedAccount struct {
@@ -757,6 +758,7 @@ func GetUserInfo(user *User, scope string, aud string, host string) *Userinfo {
} }
if strings.Contains(scope, "email") { if strings.Contains(scope, "email") {
resp.Email = user.Email resp.Email = user.Email
resp.EmailVerified = user.EmailVerified
} }
if strings.Contains(scope, "address") { if strings.Contains(scope, "address") {
resp.Address = user.Location resp.Address = user.Location

View File

@@ -35,7 +35,7 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error()) fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error())
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") { if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "did not properly respond after a period of time") {
return nil, "", nil return nil, "", nil
} else { } else {
return nil, "", err return nil, "", err

View File

@@ -16,9 +16,9 @@ package pp
import ( import (
"context" "context"
"net/http" "encoding/json"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay" "github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay" "github.com/go-pay/gopay/alipay"
) )
@@ -28,6 +28,11 @@ type AlipayPaymentProvider struct {
} }
func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) (*AlipayPaymentProvider, error) { func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) (*AlipayPaymentProvider, error) {
// clientId => appId
// cert.Certificate => appCertificate
// cert.PrivateKey => appPrivateKey
// rootCert.Certificate => authorityPublicKey
// rootCert.PrivateKey => authorityRootPublicKey
pp := &AlipayPaymentProvider{} pp := &AlipayPaymentProvider{}
client, err := alipay.NewClient(appId, appPrivateKey, true) client, err := alipay.NewClient(appId, appPrivateKey, true)
@@ -46,54 +51,60 @@ func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) { func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
// pp.Client.DebugSwitch = gopay.DebugOn // pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{} bm := gopay.BodyMap{}
pp.Client.SetReturnUrl(returnUrl)
bm.Set("providerName", providerName) pp.Client.SetNotifyUrl(notifyUrl)
bm.Set("productName", productName) bm.Set("subject", joinAttachString([]string{productName, productDisplayName, providerName}))
bm.Set("return_url", returnUrl)
bm.Set("notify_url", notifyUrl)
bm.Set("subject", productDisplayName)
bm.Set("out_trade_no", paymentName) bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", getPriceString(price)) bm.Set("total_amount", priceFloat64ToString(price))
payUrl, err := pp.Client.TradePagePay(context.Background(), bm) payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
return payUrl, "", nil return payUrl, paymentName, nil
} }
func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *AlipayPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
bm, err := alipay.ParseNotifyToBodyMap(request) bm := gopay.BodyMap{}
bm.Set("out_trade_no", orderId)
aliRsp, err := pp.Client.TradeQuery(context.Background(), bm)
notifyResult := &NotifyResult{}
if err != nil { if err != nil {
errRsp := &alipay.ErrorResponse{}
unmarshalErr := json.Unmarshal([]byte(err.Error()), errRsp)
if unmarshalErr != nil {
return nil, err
}
if errRsp.SubCode == "ACQ.TRADE_NOT_EXIST" {
notifyResult.PaymentStatus = PaymentStateCanceled
return notifyResult, nil
}
return nil, err return nil, err
} }
switch aliRsp.Response.TradeStatus {
providerName := bm.Get("providerName") case "WAIT_BUYER_PAY":
productName := bm.Get("productName") notifyResult.PaymentStatus = PaymentStateCreated
return notifyResult, nil
productDisplayName := bm.Get("subject") case "TRADE_CLOSED":
paymentName := bm.Get("out_trade_no") notifyResult.PaymentStatus = PaymentStateTimeout
price := util.ParseFloat(bm.Get("total_amount")) return notifyResult, nil
case "TRADE_SUCCESS":
ok, err := alipay.VerifySignWithCert(authorityPublicKey, bm) // skip
if err != nil { default:
return nil, err notifyResult.PaymentStatus = PaymentStateError
notifyResult.NotifyMessage = fmt.Sprintf("unexpected alipay trade state: %v", aliRsp.Response.TradeStatus)
return notifyResult, nil
} }
if !ok { productDisplayName, productName, providerName, _ := parseAttachString(aliRsp.Response.Subject)
return nil, err notifyResult = &NotifyResult{
}
notifyResult := &NotifyResult{
ProductName: productName, ProductName: productName,
ProductDisplayName: productDisplayName, ProductDisplayName: productDisplayName,
ProviderName: providerName, ProviderName: providerName,
OrderId: orderId, OrderId: orderId,
PaymentStatus: PaymentStatePaid, PaymentStatus: PaymentStatePaid,
Price: price, Price: priceStringToFloat64(aliRsp.Response.TotalAmount),
PaymentName: paymentName, PaymentName: orderId,
} }
return notifyResult, nil return notifyResult, nil
} }

View File

@@ -14,8 +14,6 @@
package pp package pp
import "net/http"
type DummyPaymentProvider struct{} type DummyPaymentProvider struct{}
func NewDummyPaymentProvider() (*DummyPaymentProvider, error) { func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
@@ -27,7 +25,7 @@ func (pp *DummyPaymentProvider) Pay(providerName string, productName string, pay
return returnUrl, "", nil return returnUrl, "", nil
} }
func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *DummyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
return &NotifyResult{ return &NotifyResult{
PaymentStatus: PaymentStatePaid, PaymentStatus: PaymentStatePaid,
}, nil }, nil

View File

@@ -216,7 +216,7 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
return payRespInfo.PayUrl, "", nil return payRespInfo.PayUrl, "", nil
} }
func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *GcPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
reqBody := GcRequestBody{} reqBody := GcRequestBody{}
m, err := url.ParseQuery(string(body)) m, err := url.ParseQuery(string(body))
if err != nil { if err != nil {

View File

@@ -18,7 +18,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strconv" "strconv"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@@ -88,7 +87,7 @@ func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, pa
return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil
} }
func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *PaypalPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
notifyResult := &NotifyResult{} notifyResult := &NotifyResult{}
captureRsp, err := pp.Client.OrderCapture(context.Background(), orderId, nil) captureRsp, err := pp.Client.OrderCapture(context.Background(), orderId, nil)
if err != nil { if err != nil {

View File

@@ -14,8 +14,6 @@
package pp package pp
import "net/http"
type PaymentState string type PaymentState string
const ( const (
@@ -42,45 +40,7 @@ type NotifyResult struct {
type PaymentProvider interface { type PaymentProvider interface {
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error)
Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) Notify(body []byte, orderId string) (*NotifyResult, error)
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
GetResponseError(err error) string GetResponseError(err error) string
} }
func GetPaymentProvider(typ string, clientId string, clientSecret string, host string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string, clientId2 string) (PaymentProvider, error) {
if typ == "Dummy" {
pp, err := NewDummyPaymentProvider()
if err != nil {
return nil, err
}
return pp, nil
} else if typ == "Alipay" {
pp, err := NewAlipayPaymentProvider(clientId, appCertificate, appPrivateKey, authorityPublicKey, authorityRootPublicKey)
if err != nil {
return nil, err
}
return pp, nil
} else if typ == "GC" {
return NewGcPaymentProvider(clientId, clientSecret, host), nil
} else if typ == "WeChat Pay" {
pp, err := NewWechatPaymentProvider(clientId, clientSecret, clientId2, appCertificate, appPrivateKey)
if err != nil {
return nil, err
}
return pp, nil
} else if typ == "PayPal" {
pp, err := NewPaypalPaymentProvider(clientId, clientSecret)
if err != nil {
return nil, err
}
return pp, nil
} else if typ == "Stripe" {
pp, err := NewStripePaymentProvider(clientId, clientSecret)
if err != nil {
return nil, err
}
return pp, nil
}
return nil, nil
}

View File

@@ -16,7 +16,6 @@ package pp
import ( import (
"fmt" "fmt"
"net/http"
"time" "time"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@@ -94,7 +93,7 @@ func (pp *StripePaymentProvider) Pay(providerName string, productName string, pa
return sCheckout.URL, sCheckout.ID, nil return sCheckout.URL, sCheckout.ID, nil
} }
func (pp *StripePaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *StripePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
notifyResult := &NotifyResult{} notifyResult := &NotifyResult{}
sCheckout, err := stripeCheckout.Get(orderId, nil) sCheckout, err := stripeCheckout.Get(orderId, nil)
if err != nil { if err != nil {

View File

@@ -49,3 +49,11 @@ func priceFloat64ToInt64(price float64) int64 {
func priceFloat64ToString(price float64) string { func priceFloat64ToString(price float64) string {
return strconv.FormatFloat(price, 'f', 2, 64) return strconv.FormatFloat(price, 'f', 2, 64)
} }
func priceStringToFloat64(price string) float64 {
f, err := strconv.ParseFloat(price, 64)
if err != nil {
panic(err)
}
return f
}

View File

@@ -17,7 +17,7 @@ package pp
import ( import (
"context" "context"
"errors" "errors"
"net/http" "fmt"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay" "github.com/go-pay/gopay"
@@ -31,17 +31,22 @@ type WechatPayNotifyResponse struct {
type WechatPaymentProvider struct { type WechatPaymentProvider struct {
Client *wechat.ClientV3 Client *wechat.ClientV3
appId string AppId string
} }
func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, mchCertSerialNumber string, privateKey string) (*WechatPaymentProvider, error) { func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, serialNo string, privateKey string) (*WechatPaymentProvider, error) {
if appId == "" && mchId == "" && mchCertSerialNumber == "" && apiV3Key == "" && privateKey == "" { // https://pay.weixin.qq.com/docs/merchant/products/native-payment/preparation.html
// clientId => mchId
// clientSecret => apiV3Key
// clientId2 => appId
// appCertificate => serialNo
// appPrivateKey => privateKey
if appId == "" || mchId == "" || serialNo == "" || apiV3Key == "" || privateKey == "" {
return &WechatPaymentProvider{}, nil return &WechatPaymentProvider{}, nil
} }
pp := &WechatPaymentProvider{appId: appId} clientV3, err := wechat.NewClientV3(mchId, serialNo, apiV3Key, privateKey)
clientV3, err := wechat.NewClientV3(mchId, mchCertSerialNumber, apiV3Key, privateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -50,73 +55,70 @@ func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, mchCe
if err != nil { if err != nil {
return nil, err return nil, err
} }
pp := &WechatPaymentProvider{
pp.Client = clientV3.SetPlatformCert([]byte(platformCert), serialNo) Client: clientV3.SetPlatformCert([]byte(platformCert), serialNo),
AppId: appId,
}
return pp, nil return pp, nil
} }
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) { func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{} bm := gopay.BodyMap{}
bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName})) bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName}))
bm.Set("appid", pp.appId) bm.Set("appid", pp.AppId)
bm.Set("description", productDisplayName) bm.Set("description", productDisplayName)
bm.Set("notify_url", notifyUrl) bm.Set("notify_url", notifyUrl)
bm.Set("out_trade_no", paymentName) bm.Set("out_trade_no", paymentName)
bm.SetBodyMap("amount", func(bm gopay.BodyMap) { bm.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", int(price*100)) bm.Set("total", priceFloat64ToInt64(price))
bm.Set("currency", "CNY") bm.Set("currency", currency)
}) })
wxRsp, err := pp.Client.V3TransactionNative(context.Background(), bm) nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
if nativeRsp.Code != wechat.Success {
if wxRsp.Code != wechat.Success { return "", "", errors.New(nativeRsp.Error)
return "", "", errors.New(wxRsp.Error)
} }
return wxRsp.Response.CodeUrl, "", nil return nativeRsp.Response.CodeUrl, paymentName, nil // Wechat can use paymentName as the OutTradeNo to query order status
} }
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *WechatPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
notifyReq, err := wechat.V3ParseNotify(request) notifyResult := &NotifyResult{}
queryRsp, err := pp.Client.V3TransactionQueryOrder(context.Background(), wechat.OutTradeNo, orderId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if queryRsp.Code != wechat.Success {
cert := pp.Client.WxPublicKey() return nil, errors.New(queryRsp.Error)
err = notifyReq.VerifySignByPK(cert)
if err != nil {
return nil, err
} }
apiKey := string(pp.Client.ApiV3Key) switch queryRsp.Response.TradeState {
result, err := notifyReq.DecryptCipherText(apiKey) case "SUCCESS":
if err != nil { // skip
return nil, err case "CLOSED":
notifyResult.PaymentStatus = PaymentStateCanceled
return notifyResult, nil
case "NOTPAY", "USERPAYING": // not-pad: waiting for user to pay; user-paying: user is paying
notifyResult.PaymentStatus = PaymentStateCreated
return notifyResult, nil
default:
notifyResult.PaymentStatus = PaymentStateError
notifyResult.NotifyMessage = fmt.Sprintf("unexpected wechat trade state: %v", queryRsp.Response.TradeState)
return notifyResult, nil
} }
productDisplayName, productName, providerName, _ := parseAttachString(queryRsp.Response.Attach)
paymentName := result.OutTradeNo notifyResult = &NotifyResult{
price := float64(result.Amount.PayerTotal) / 100
productDisplayName, productName, providerName, err := parseAttachString(result.Attach)
if err != nil {
return nil, err
}
notifyResult := &NotifyResult{
ProductName: productName, ProductName: productName,
ProductDisplayName: productDisplayName, ProductDisplayName: productDisplayName,
ProviderName: providerName, ProviderName: providerName,
OrderId: orderId, OrderId: orderId,
Price: price, Price: priceInt64ToFloat64(int64(queryRsp.Response.Amount.Total)),
PaymentStatus: PaymentStatePaid, PaymentStatus: PaymentStatePaid,
PaymentName: paymentName, PaymentName: queryRsp.Response.OutTradeNo,
} }
return notifyResult, nil return notifyResult, nil
} }

View File

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

View File

@@ -40,6 +40,13 @@ func CorsFilter(ctx *context.Context) {
return return
} }
if ctx.Request.RequestURI == "/api/userinfo" {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
return
}
if origin != "" && originConf != "" && origin != originConf { if origin != "" && originConf != "" && origin != originConf {
ok, err := object.IsOriginAllowed(origin) ok, err := object.IsOriginAllowed(origin)
if err != nil { if err != nil {

View File

@@ -247,10 +247,10 @@ func initAPI() {
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms") beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")
beego.Router("/api/send-notification", &controllers.ApiController{}, "POST:SendNotification") beego.Router("/api/send-notification", &controllers.ApiController{}, "POST:SendNotification")
beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "Get:WebAuthnSignupBegin") beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "GET:WebAuthnSignupBegin")
beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "Post:WebAuthnSignupFinish") beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "POST:WebAuthnSignupFinish")
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin") beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "GET:WebAuthnSigninBegin")
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish") beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "POST:WebAuthnSigninFinish")
beego.Router("/api/mfa/setup/initiate", &controllers.ApiController{}, "POST:MfaSetupInitiate") beego.Router("/api/mfa/setup/initiate", &controllers.ApiController{}, "POST:MfaSetupInitiate")
beego.Router("/api/mfa/setup/verify", &controllers.ApiController{}, "POST:MfaSetupVerify") beego.Router("/api/mfa/setup/verify", &controllers.ApiController{}, "POST:MfaSetupVerify")

View File

@@ -15,6 +15,7 @@
package storage package storage
import ( import (
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -23,76 +24,69 @@ import (
"github.com/casdoor/oss" "github.com/casdoor/oss"
) )
var baseFolder = "files" // LocalFileSystemProvider file system storage
type LocalFileSystemProvider struct {
// FileSystem file system storage BaseDir string
type FileSystem struct {
Base string
} }
// NewFileSystem initialize the local file system storage // NewLocalFileSystemStorageProvider initialize the local file system storage
func NewFileSystem(base string) *FileSystem { func NewLocalFileSystemStorageProvider() *LocalFileSystemProvider {
absBase, err := filepath.Abs(base) baseFolder := "files"
absBase, err := filepath.Abs(baseFolder)
if err != nil { if err != nil {
panic("local file system storage's base folder is not initialized") panic(err)
} }
return &FileSystem{Base: absBase} return &LocalFileSystemProvider{BaseDir: absBase}
} }
// GetFullPath get full path from absolute/relative path // GetFullPath get full path from absolute/relative path
func (fileSystem FileSystem) GetFullPath(path string) string { func (sp LocalFileSystemProvider) GetFullPath(path string) string {
fullPath := path fullPath := path
if !strings.HasPrefix(path, fileSystem.Base) { if !strings.HasPrefix(path, sp.BaseDir) {
fullPath, _ = filepath.Abs(filepath.Join(fileSystem.Base, path)) fullPath, _ = filepath.Abs(filepath.Join(sp.BaseDir, path))
} }
return fullPath return fullPath
} }
// Get receive file with given path // Get receive file with given path
func (fileSystem FileSystem) Get(path string) (*os.File, error) { func (sp LocalFileSystemProvider) Get(path string) (*os.File, error) {
return os.Open(fileSystem.GetFullPath(path)) return os.Open(sp.GetFullPath(path))
} }
// GetStream get file as stream // GetStream get file as stream
func (fileSystem FileSystem) GetStream(path string) (io.ReadCloser, error) { func (sp LocalFileSystemProvider) GetStream(path string) (io.ReadCloser, error) {
return os.Open(fileSystem.GetFullPath(path)) return os.Open(sp.GetFullPath(path))
} }
// Put store a reader into given path // Put store a reader into given path
func (fileSystem FileSystem) Put(path string, reader io.Reader) (*oss.Object, error) { func (sp LocalFileSystemProvider) Put(path string, reader io.Reader) (*oss.Object, error) {
var ( fullPath := sp.GetFullPath(path)
fullPath = fileSystem.GetFullPath(path)
err = os.MkdirAll(filepath.Dir(fullPath), os.ModePerm)
)
err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("Casdoor fails to create folder: \"%s\" for local file system storage provider: %s. Make sure Casdoor process has correct permission to create/access it, or you can create it manually in advance", filepath.Dir(fullPath), err.Error())
} }
dst, err := os.Create(filepath.Clean(fullPath)) dst, err := os.Create(filepath.Clean(fullPath))
if err == nil { if err == nil {
if seeker, ok := reader.(io.ReadSeeker); ok { if seeker, ok := reader.(io.ReadSeeker); ok {
seeker.Seek(0, 0) seeker.Seek(0, 0)
} }
_, err = io.Copy(dst, reader) _, err = io.Copy(dst, reader)
} }
return &oss.Object{Path: path, Name: filepath.Base(path), StorageInterface: sp}, err
return &oss.Object{Path: path, Name: filepath.Base(path), StorageInterface: fileSystem}, err
} }
// Delete delete file // Delete delete file
func (fileSystem FileSystem) Delete(path string) error { func (sp LocalFileSystemProvider) Delete(path string) error {
return os.Remove(fileSystem.GetFullPath(path)) return os.Remove(sp.GetFullPath(path))
} }
// List list all objects under current path // List list all objects under current path
func (fileSystem FileSystem) List(path string) ([]*oss.Object, error) { func (sp LocalFileSystemProvider) List(path string) ([]*oss.Object, error) {
var ( objects := []*oss.Object{}
objects []*oss.Object fullPath := sp.GetFullPath(path)
fullPath = fileSystem.GetFullPath(path)
)
filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error { filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
if path == fullPath { if path == fullPath {
@@ -102,10 +96,10 @@ func (fileSystem FileSystem) List(path string) ([]*oss.Object, error) {
if err == nil && !info.IsDir() { if err == nil && !info.IsDir() {
modTime := info.ModTime() modTime := info.ModTime()
objects = append(objects, &oss.Object{ objects = append(objects, &oss.Object{
Path: strings.TrimPrefix(path, fileSystem.Base), Path: strings.TrimPrefix(path, sp.BaseDir),
Name: info.Name(), Name: info.Name(),
LastModified: &modTime, LastModified: &modTime,
StorageInterface: fileSystem, StorageInterface: sp,
}) })
} }
return nil return nil
@@ -114,16 +108,12 @@ func (fileSystem FileSystem) List(path string) ([]*oss.Object, error) {
return objects, nil return objects, nil
} }
// GetEndpoint get endpoint, FileSystem's endpoint is / // GetEndpoint get endpoint, LocalFileSystemProvider's endpoint is /
func (fileSystem FileSystem) GetEndpoint() string { func (sp LocalFileSystemProvider) GetEndpoint() string {
return "/" return "/"
} }
// GetURL get public accessible URL // GetURL get public accessible URL
func (fileSystem FileSystem) GetURL(path string) (url string, err error) { func (sp LocalFileSystemProvider) GetURL(path string) (url string, err error) {
return path, nil return path, nil
} }
func NewLocalFileSystemStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
return NewFileSystem(baseFolder)
}

View File

@@ -19,7 +19,7 @@ import "github.com/casdoor/oss"
func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface { func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
switch providerType { switch providerType {
case "Local File System": case "Local File System":
return NewLocalFileSystemStorageProvider(clientId, clientSecret, region, bucket, endpoint) return NewLocalFileSystemStorageProvider()
case "AWS S3": case "AWS S3":
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint) return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "MinIO": case "MinIO":

View File

@@ -187,32 +187,6 @@ func IsStringsEmpty(strs ...string) bool {
return false return false
} }
func GetMaxLenStr(strs ...string) string {
m := 0
i := 0
for j, str := range strs {
l := len(str)
if l > m {
m = l
i = j
}
}
return strs[i]
}
func GetMinLenStr(strs ...string) string {
m := int(^uint(0) >> 1)
i := 0
for j, str := range strs {
l := len(str)
if l < m {
m = l
i = j
}
}
return strs[i]
}
func ReadStringFromPath(path string) string { func ReadStringFromPath(path string) string {
data, err := os.ReadFile(filepath.Clean(path)) data, err := os.ReadFile(filepath.Clean(path))
if err != nil { if err != nil {

View File

@@ -189,45 +189,6 @@ func TestIsStrsEmpty(t *testing.T) {
} }
} }
func TestGetMaxLenStr(t *testing.T) {
scenarios := []struct {
description string
input []string
expected interface{}
}{
{"Should be return casdoor", []string{"", "casdoor", "casbin"}, "casdoor"},
{"Should be return casdoor_jdk", []string{"", "casdoor", "casbin", "casdoor_jdk"}, "casdoor_jdk"},
{"Should be return empty string", []string{""}, ""},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetMaxLenStr(scenery.input...)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestGetMinLenStr(t *testing.T) {
scenarios := []struct {
description string
input []string
expected interface{}
}{
{"Should be return casbin", []string{"casdoor", "casbin"}, "casbin"},
{"Should be return casbin", []string{"casdoor", "casbin", "casdoor_jdk"}, "casbin"},
{"Should be return empty string", []string{"a", "", "casbin"}, ""},
{"Should be return a", []string{"a", "casdoor", "casbin"}, "a"},
{"Should be return a", []string{"casdoor", "a", "casbin"}, "a"},
{"Should be return a", []string{"casbin", "casdoor", "a"}, "a"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetMinLenStr(scenery.input...)
assert.Equal(t, scenery.expected, actual, "The returned value not is expected")
})
}
}
func TestSnakeString(t *testing.T) { func TestSnakeString(t *testing.T) {
scenarios := []struct { scenarios := []struct {
description string description string

View File

@@ -34,6 +34,7 @@
"i18next": "^19.8.9", "i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19", "libphonenumber-js": "^1.10.19",
"moment": "^2.29.1", "moment": "^2.29.1",
"qrcode.react": "^3.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-app-polyfill": "^3.0.0", "react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
@@ -82,6 +83,9 @@
"@babel/eslint-parser": "^7.18.9", "@babel/eslint-parser": "^7.18.9",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^12.5.1", "cypress": "^12.5.1",
"eslint": "8.22.0", "eslint": "8.22.0",
@@ -91,10 +95,7 @@
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"stylelint": "^14.11.0", "stylelint": "^14.11.0",
"stylelint-config-recommended-less": "^1.0.4", "stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^28.0.0", "stylelint-config-standard": "^28.0.0"
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2"
}, },
"lint-staged": { "lint-staged": {
"src/**/*.{css,less}": [ "src/**/*.{css,less}": [

View File

@@ -664,7 +664,8 @@ class App extends Component {
window.location.pathname.startsWith("/cas") || window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/auto-signup") || window.location.pathname.startsWith("/auto-signup") ||
window.location.pathname.startsWith("/select-plan") || window.location.pathname.startsWith("/select-plan") ||
window.location.pathname.startsWith("/buy-plan"); window.location.pathname.startsWith("/buy-plan") ||
window.location.pathname.startsWith("/qrcode") ;
} }
renderPage() { renderPage() {

View File

@@ -542,7 +542,7 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("signup:Terms of Use"), i18next.t("signup:Terms of Use - Tooltip"))} : {Setting.getLabel(i18next.t("signup:Terms of Use"), i18next.t("signup:Terms of Use - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.application.termsOfUse} style={{marginBottom: "10px"}} onChange={e => { <Input prefix={<LinkOutlined />} value={this.state.application.termsOfUse} style={{marginBottom: "10px"}} onChange={e => {
this.updateApplicationField("termsOfUse", e.target.value); this.updateApplicationField("termsOfUse", e.target.value);
}} /> }} />
<Upload maxCount={1} accept=".html" showUploadList={false} <Upload maxCount={1} accept=".html" showUploadList={false}

View File

@@ -204,7 +204,7 @@ class BaseListPage extends React.Component {
this.renderTable(this.state.data) this.renderTable(this.state.data)
} }
<Tour <Tour
open={this.state.isTourVisible} open={Setting.isMobile() ? false : this.state.isTourVisible}
onClose={this.setIsTourVisible} onClose={this.setIsTourVisible}
steps={this.getSteps()} steps={this.getSteps()}
indicatorsRender={(current, total) => ( indicatorsRender={(current, total) => (

View File

@@ -31,6 +31,7 @@ import CasLogout from "./auth/CasLogout";
import {authConfig} from "./auth/Auth"; import {authConfig} from "./auth/Auth";
import ProductBuyPage from "./ProductBuyPage"; import ProductBuyPage from "./ProductBuyPage";
import PaymentResultPage from "./PaymentResultPage"; import PaymentResultPage from "./PaymentResultPage";
import QrCodePage from "./QrCodePage";
class EntryPage extends React.Component { class EntryPage extends React.Component {
constructor(props) { constructor(props) {
@@ -113,6 +114,7 @@ class EntryPage extends React.Component {
<Route exact path="/select-plan/:owner/:pricingName" render={(props) => <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" 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} />} /> <Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
<Route exact path="/qrcode/:owner/:paymentName" render={(props) => <QrCodePage {...this.props} onUpdateApplication={onUpdateApplication} {...props} />} />
</Switch> </Switch>
</div> </div>
); );

View File

@@ -101,7 +101,7 @@ class PaymentResultPage extends React.Component {
payment: payment, payment: payment,
}); });
if (payment.state === "Created") { if (payment.state === "Created") {
if (["PayPal", "Stripe"].includes(payment.type)) { if (["PayPal", "Stripe", "Alipay"].includes(payment.type)) {
this.setState({ this.setState({
timeout: setTimeout(async() => { timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName); await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);

View File

@@ -17,6 +17,7 @@ import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import * as PermissionBackend from "./backend/PermissionBackend"; import * as PermissionBackend from "./backend/PermissionBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
import * as GroupBackend from "./backend/GroupBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend"; import * as RoleBackend from "./backend/RoleBackend";
@@ -35,6 +36,7 @@ class PermissionEditPage extends React.Component {
organizations: [], organizations: [],
model: null, model: null,
users: [], users: [],
groups: [],
roles: [], roles: [],
models: [], models: [],
resources: [], resources: [],
@@ -67,6 +69,7 @@ class PermissionEditPage extends React.Component {
}); });
this.getUsers(permission.owner); this.getUsers(permission.owner);
this.getGroups(permission.owner);
this.getRoles(permission.owner); this.getRoles(permission.owner);
this.getModels(permission.owner); this.getModels(permission.owner);
this.getResources(permission.owner); this.getResources(permission.owner);
@@ -97,6 +100,20 @@ class PermissionEditPage extends React.Component {
}); });
} }
getGroups(organizationName) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
groups: res.data,
});
});
}
getRoles(organizationName) { getRoles(organizationName) {
RoleBackend.getRoles(organizationName) RoleBackend.getRoles(organizationName)
.then((res) => { .then((res) => {
@@ -192,6 +209,7 @@ class PermissionEditPage extends React.Component {
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.permission.owner} onChange={(owner => { <Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.permission.owner} onChange={(owner => {
this.updatePermissionField("owner", owner); this.updatePermissionField("owner", owner);
this.getUsers(owner); this.getUsers(owner);
this.getGroups(owner);
this.getRoles(owner); this.getRoles(owner);
this.getModels(owner); this.getModels(owner);
this.getResources(owner); this.getResources(owner);
@@ -263,6 +281,17 @@ class PermissionEditPage extends React.Component {
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub groups"), i18next.t("role:Sub groups - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.groups}
onChange={(value => {this.updatePermissionField("groups", value);})}
options={this.state.groups.map((group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} : {Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :

View File

@@ -33,6 +33,7 @@ class PermissionListPage extends BaseListPage {
createdTime: moment().format(), createdTime: moment().format(),
displayName: `New Permission - ${randomName}`, displayName: `New Permission - ${randomName}`,
users: [`${this.props.account.owner}/${this.props.account.name}`], users: [`${this.props.account.owner}/${this.props.account.name}`],
groups: [],
roles: [], roles: [],
domains: [], domains: [],
resourceType: "Application", resourceType: "Application",
@@ -179,6 +180,17 @@ class PermissionListPage extends BaseListPage {
return Setting.getTags(text, "users"); return Setting.getTags(text, "users");
}, },
}, },
{
title: i18next.t("role:Sub groups"),
dataIndex: "groups",
key: "groups",
// width: '100px',
sorter: true,
...this.getColumnSearchProps("groups"),
render: (text, record, index) => {
return Setting.getTags(text, "groups");
},
},
{ {
title: i18next.t("role:Sub roles"), title: i18next.t("role:Sub roles"),
dataIndex: "roles", dataIndex: "roles",

View File

@@ -13,8 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Descriptions, Modal, Spin} from "antd"; import {Button, Descriptions, Spin} from "antd";
import {CheckCircleTwoTone} from "@ant-design/icons";
import i18next from "i18next"; import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend"; import * as ProductBackend from "./backend/ProductBackend";
import * as PlanBackend from "./backend/PlanBackend"; import * as PlanBackend from "./backend/PlanBackend";
@@ -36,7 +35,6 @@ class ProductBuyPage extends React.Component {
pricing: props?.pricing ?? null, pricing: props?.pricing ?? null,
plan: null, plan: null,
isPlacingOrder: false, isPlacingOrder: false,
qrCodeModalProvider: null,
}; };
} }
@@ -130,13 +128,6 @@ class ProductBuyPage extends React.Component {
} }
buyProduct(product, provider) { buyProduct(product, provider) {
if (provider.clientId.startsWith("http")) {
this.setState({
qrCodeModalProvider: provider,
});
return;
}
this.setState({ this.setState({
isPlacingOrder: true, isPlacingOrder: true,
}); });
@@ -144,7 +135,11 @@ class ProductBuyPage extends React.Component {
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "") ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "")
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const payUrl = res.data; const payment = res.data;
let payUrl = payment.payUrl;
if (provider.type === "WeChat Pay") {
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURI(payment.payUrl)}&successUrl=${encodeURI(payment.successUrl)}`;
}
Setting.goToLink(payUrl); Setting.goToLink(payUrl);
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
@@ -159,45 +154,6 @@ class ProductBuyPage extends React.Component {
}); });
} }
renderQrCodeModal() {
if (this.state.qrCodeModalProvider === undefined || this.state.qrCodeModalProvider === null) {
return null;
}
return (
<Modal title={
<div>
<CheckCircleTwoTone twoToneColor="rgb(45,120,213)" />
{" " + i18next.t("product:Please scan the QR code to pay")}
</div>
}
open={this.state.qrCodeModalProvider !== undefined && this.state.qrCodeModalProvider !== null}
onOk={() => {
Setting.goToLink(this.state.product.returnUrl);
}}
onCancel={() => {
this.setState({
qrCodeModalProvider: null,
});
}}
okText={i18next.t("product:I have completed the payment")}
cancelText={i18next.t("general:Cancel")}>
<p key={this.state.qrCodeModalProvider?.name}>
{
i18next.t("product:Please provide your username in the remark")
}
:&nbsp;&nbsp;
{
Setting.getTag("default", this.props.account.name)
}
<br />
<br />
<img src={this.state.qrCodeModalProvider?.clientId} alt={this.state.qrCodeModalProvider?.name} width={"472px"} style={{marginBottom: "20px"}} />
</p>
</Modal>
);
}
getPayButton(provider) { getPayButton(provider) {
let text = provider.type; let text = provider.type;
if (provider.type === "Dummy") { if (provider.type === "Dummy") {
@@ -290,9 +246,6 @@ class ProductBuyPage extends React.Component {
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Spin> </Spin>
{
this.renderQrCodeModal()
}
</div> </div>
); );
} }

View File

@@ -16,6 +16,8 @@ import React from "react";
import {Button, Card, Checkbox, Col, Input, InputNumber, Row, Select, Switch} from "antd"; import {Button, Card, Checkbox, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import {LinkOutlined} from "@ant-design/icons"; import {LinkOutlined} from "@ant-design/icons";
import * as ProviderBackend from "./backend/ProviderBackend"; import * as ProviderBackend from "./backend/ProviderBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import {authConfig} from "./auth/Auth"; import {authConfig} from "./auth/Auth";
@@ -24,7 +26,6 @@ import * as ProviderNotification from "./common/TestNotificationWidget";
import * as ProviderEditTestSms from "./common/TestSmsWidget"; import * as ProviderEditTestSms from "./common/TestSmsWidget";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import {CaptchaPreview} from "./common/CaptchaPreview"; import {CaptchaPreview} from "./common/CaptchaPreview";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect"; import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import * as Web3Auth from "./auth/Web3Auth"; import * as Web3Auth from "./auth/Web3Auth";
@@ -39,6 +40,7 @@ class ProviderEditPage extends React.Component {
providerName: props.match.params.providerName, providerName: props.match.params.providerName,
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName, owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
provider: null, provider: null,
certs: [],
organizations: [], organizations: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
@@ -47,6 +49,7 @@ class ProviderEditPage extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getOrganizations(); this.getOrganizations();
this.getProvider(); this.getProvider();
this.getCerts(this.state.owner);
} }
getProvider() { getProvider() {
@@ -80,6 +83,17 @@ class ProviderEditPage extends React.Component {
} }
} }
getCerts(owner) {
CertBackend.getCerts(owner)
.then((res) => {
if (res.status === "ok") {
this.setState({
certs: res.data || [],
});
}
});
}
parseProviderField(key, value) { parseProviderField(key, value) {
if (["port"].includes(key)) { if (["port"].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
@@ -91,6 +105,11 @@ class ProviderEditPage extends React.Component {
value = this.parseProviderField(key, value); value = this.parseProviderField(key, value);
const provider = this.state.provider; const provider = this.state.provider;
if (key === "owner" && provider["owner"] !== value) {
// the provider change the owner, reset the cert
provider["cert"] = "";
this.getCerts(value);
}
provider[key] = value; provider[key] = value;
this.setState({ this.setState({
provider: provider, provider: provider,
@@ -182,6 +201,12 @@ class ProviderEditPage extends React.Component {
} else { } else {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip")); return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
} }
case "Notification":
if (provider.type === "Line") {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
default: default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
} }
@@ -272,12 +297,15 @@ class ProviderEditPage extends React.Component {
tooltip = i18next.t("provider:Project Id - Tooltip"); tooltip = i18next.t("provider:Project Id - Tooltip");
} }
} else if (provider.category === "Email") { } else if (provider.category === "Email") {
if (provider.type === "SUBMAIL") { if (provider.type === "SUBMAIL" || provider.type === "Azure ACS") {
text = i18next.t("provider:App ID"); text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip"); tooltip = i18next.t("provider:App ID - Tooltip");
} }
} else if (provider.category === "Notification") { } else if (provider.category === "Notification") {
if (provider.type === "Telegram") { if (provider.type === "Viber") {
text = i18next.t("provider:Domain");
tooltip = i18next.t("provider:Domain - Tooltip");
} else if (provider.type === "Telegram" || provider.type === "DingTalk" || provider.type === "Pushover" || provider.type === "Pushbullet" || provider.type === "Slack" || provider.type === "Discord" || provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") {
text = i18next.t("provider:App Key"); text = i18next.t("provider:App Key");
tooltip = i18next.t("provider:App Key - Tooltip"); tooltip = i18next.t("provider:App Key - Tooltip");
} }
@@ -305,16 +333,25 @@ class ProviderEditPage extends React.Component {
let text = ""; let text = "";
let tooltip = ""; let tooltip = "";
if (provider.type === "Telegram") { if (provider.type === "Telegram" || provider.type === "Pushover" || provider.type === "Pushbullet" || provider.type === "Slack" || provider.type === "Discord" || provider.type === "Line" || provider.type === "Twitter" || provider.type === "Reddit" || provider.type === "Rocket Chat" || provider.type === "Viber") {
text = i18next.t("provider:Chat ID"); text = i18next.t("provider:Chat ID");
tooltip = i18next.t("provider:Chat ID - Tooltip"); tooltip = i18next.t("provider:Chat ID - Tooltip");
} else if (provider.type === "Custom HTTP") { } else if (provider.type === "Custom HTTP" || provider.type === "Lark" || provider.type === "Microsoft Teams" || provider.type === "Webpush" || provider.type === "Matrix") {
text = i18next.t("provider:Endpoint"); text = i18next.t("provider:Endpoint");
tooltip = i18next.t("provider:Endpoint - Tooltip"); tooltip = i18next.t("provider:Endpoint - Tooltip");
} else if (provider.type === "DingTalk" || provider.type === "Bark") {
text = i18next.t("provider:Secret Key");
tooltip = i18next.t("provider:Secret Key - Tooltip");
} }
if (text === "" && tooltip === "") { if (text === "" && tooltip === "") {
return null; return (
<React.Fragment>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Test Notification", "Test Notification")} :
</Col>
</React.Fragment>
);
} else { } else {
return ( return (
<React.Fragment> <React.Fragment>
@@ -406,7 +443,6 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Twilio SMS"); this.updateProviderField("type", "Twilio SMS");
} else if (value === "Storage") { } else if (value === "Storage") {
this.updateProviderField("type", "AWS S3"); this.updateProviderField("type", "AWS S3");
this.updateProviderField("domain", Setting.getFullServerUrl());
} else if (value === "SAML") { } else if (value === "SAML") {
this.updateProviderField("type", "Keycloak"); this.updateProviderField("type", "Keycloak");
} else if (value === "Payment") { } else if (value === "Payment") {
@@ -590,19 +626,25 @@ class ProviderEditPage extends React.Component {
} }
{ {
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") || (this.state.provider.category === "Captcha" && this.state.provider.type === "Default") ||
(this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") ||
(this.state.provider.category === "Web3") || (this.state.provider.category === "Web3") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System" || (this.state.provider.category === "Notification")) ? null : ( (this.state.provider.category === "Storage" && this.state.provider.type === "Local File System" ||
(this.state.provider.category === "Notification" && this.state.provider.type !== "Webpush" && this.state.provider.type !== "Line" && this.state.provider.type !== "Matrix" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" && this.state.provider.type !== "Rocket Chat" && this.state.provider.type !== "Viber")) ? null : (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> this.state.provider.type === "Line" ? null : (
{this.getClientIdLabel(this.state.provider)} : <Row style={{marginTop: "20px"}} >
</Col> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
<Col span={22} > {this.getClientIdLabel(this.state.provider)} :
<Input value={this.state.provider.clientId} onChange={e => { </Col>
this.updateProviderField("clientId", e.target.value); <Col span={22} >
}} /> <Input value={this.state.provider.clientId} onChange={e => {
</Col> this.updateProviderField("clientId", e.target.value);
</Row> }} />
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecretLabel(this.state.provider)} : {this.getClientSecretLabel(this.state.provider)} :
@@ -617,7 +659,7 @@ class ProviderEditPage extends React.Component {
) )
} }
{ {
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" ? null : ( this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" ? null : (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@@ -630,7 +672,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
{ {
this.state.provider.type === "WeChat Pay" ? null : ( (this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecret2Label(this.state.provider)} : {this.getClientSecret2Label(this.state.provider)} :
@@ -783,6 +825,18 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
) : null} ) : null}
{["Google Chat"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={4} value={this.state.provider.metadata} onChange={e => {
this.updateProviderField("metadata", e.target.value);
}} />
</Col>
</Row>
) : null}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Content"), i18next.t("provider:Content - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Content"), i18next.t("provider:Content - Tooltip"))} :
@@ -813,26 +867,30 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > {["Azure ACS"].includes(this.state.provider.type) ? null : (
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Row style={{marginTop: "20px"}} >
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} : <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col> {Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
<Col span={22} > </Col>
<InputNumber value={this.state.provider.port} onChange={value => { <Col span={22} >
this.updateProviderField("port", value); <InputNumber value={this.state.provider.port} onChange={value => {
}} /> this.updateProviderField("port", value);
</Col> }} />
</Row> </Col>
<Row style={{marginTop: "20px"}} > </Row>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> )}
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} : {["Azure ACS"].includes(this.state.provider.type) ? null : (
</Col> <Row style={{marginTop: "20px"}} >
<Col span={1} > <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
<Switch checked={this.state.provider.disableSsl} onChange={checked => { {Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
this.updateProviderField("disableSsl", checked); </Col>
}} /> <Col span={1} >
</Col> <Switch checked={this.state.provider.disableSsl} onChange={checked => {
</Row> this.updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
@@ -862,9 +920,11 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("receiver", e.target.value); this.updateProviderField("receiver", e.target.value);
}} /> }} />
</Col> </Col>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} > {["Azure ACS"].includes(this.state.provider.type) ? null : (
{i18next.t("provider:Test SMTP Connection")} <Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
</Button> {i18next.t("provider:Test SMTP Connection")}
</Button>
)}
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" <Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidEmail(this.state.provider.receiver)} disabled={!Setting.isValidEmail(this.state.provider.receiver)}
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.provider.receiver)} > onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.provider.receiver)} >
@@ -909,15 +969,15 @@ class ProviderEditPage extends React.Component {
<Col span={4} > <Col span={4} >
<Input.Group compact> <Input.Group compact>
<CountryCodeSelect <CountryCodeSelect
style={{width: "30%"}} style={{width: "90px"}}
value={this.state.provider.content} initValue={this.state.provider.content}
onChange={(value) => { onChange={(value) => {
this.updateProviderField("content", value); this.updateProviderField("content", value);
}} }}
countryCodes={this.props.account.organization.countryCodes} countryCodes={this.props.account.organization.countryCodes}
/> />
<Input value={this.state.provider.receiver} <Input value={this.state.provider.receiver}
style={{width: "70%"}} style={{width: "150px"}}
placeholder = {i18next.t("user:Input your phone number")} placeholder = {i18next.t("user:Input your phone number")}
onChange={e => { onChange={e => {
this.updateProviderField("receiver", e.target.value); this.updateProviderField("receiver", e.target.value);
@@ -1042,9 +1102,27 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} : {Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.provider.cert} onChange={e => { <Select virtual={false} style={{width: "100%"}} value={this.state.provider.cert} onChange={(value => {this.updateProviderField("cert", value);})}>
this.updateProviderField("cert", e.target.value); {
}} /> this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
}
</Select>
</Col>
</Row>
) : null
}
{
(this.state.provider.type === "Alipay") ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Root Cert"), i18next.t("general:Root Cert - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.metadata} onChange={(value => {this.updateProviderField("metadata", value);})}>
{
this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
}
</Select>
</Col> </Col>
</Row> </Row>
) : null ) : null

135
web/src/QrCodePage.js Normal file
View File

@@ -0,0 +1,135 @@
// 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 QRCode from "qrcode.react";
import {Button, Col, Row} from "antd";
import * as PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting";
import * as ProviderBackend from "./backend/ProviderBackend";
import i18next from "i18next";
class QrCodePage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(window.location.search);
this.state = {
classes: props,
owner: props.owner ?? (props.match?.params?.owner ?? null),
paymentName: props.paymentName ?? (props.match?.params?.paymentName ?? null),
providerName: props.providerName ?? params.get("providerName"),
payUrl: props.payUrl ?? params.get("payUrl"),
successUrl: props.successUrl ?? params.get("successUrl"),
provider: props.provider ?? null,
payment: null,
timer: null,
};
}
async getProvider() {
if (!this.state.owner || !this.state.providerName) {
return ;
}
try {
const res = await ProviderBackend.getProvider(this.state.owner, this.state.providerName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const provider = res.data;
this.setState({
provider: provider,
});
} catch (err) {
Setting.showMessage("error", err.message);
return ;
}
}
setNotifyTask() {
if (!this.state.owner || !this.state.paymentName) {
return ;
}
const notifyTask = async() => {
try {
const res = await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const payment = res.data;
if (payment.state !== "Created") {
Setting.goToLink(this.state.successUrl);
}
} catch (err) {
Setting.showMessage("error", err.message);
return ;
}
};
this.setState({
timer: setTimeout(async() => {
await notifyTask();
this.setNotifyTask();
}, 2000),
});
}
componentDidMount() {
if (this.props.onUpdateApplication) {
this.props.onUpdateApplication(null);
}
this.getProvider();
this.setNotifyTask();
}
componentWillUnmount() {
clearInterval(this.state.timer);
}
renderProviderInfo(provider) {
if (!provider) {
return null;
}
const text = i18next.t(`product:${provider.type}`);
return (
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
} size={"large"} >
{
text
}
</Button>
);
}
render() {
if (!this.state.payUrl || !this.state.successUrl || !this.state.owner || !this.state.paymentName) {
return null;
}
return (
<div className="login-content">
<Col>
<Row style={{justifyContent: "center"}}>
{this.renderProviderInfo(this.state.provider)}
</Row>
<Row style={{marginTop: "10px", justifyContent: "center"}}>
<QRCode value={this.state.payUrl} size={this.props.size ?? 200} />
</Row>
</Col>
</div>
);
}
}
export default QrCodePage;

View File

@@ -161,6 +161,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/email_mailtrap.png`, logo: `${StaticBaseUrl}/img/email_mailtrap.png`,
url: "https://mailtrap.io", url: "https://mailtrap.io",
}, },
"Azure ACS": {
logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://learn.microsoft.com/zh-cn/azure/communication-services",
},
}, },
Storage: { Storage: {
"Local File System": { "Local File System": {
@@ -283,6 +287,70 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/email_default.png`, logo: `${StaticBaseUrl}/img/email_default.png`,
url: "https://casdoor.org/docs/provider/notification/overview", url: "https://casdoor.org/docs/provider/notification/overview",
}, },
"DingTalk": {
logo: `${StaticBaseUrl}/img/social_dingtalk.png`,
url: "https://www.dingtalk.com/",
},
"Lark": {
logo: `${StaticBaseUrl}/img/social_lark.png`,
url: "https://www.larksuite.com/",
},
"Microsoft Teams": {
logo: `${StaticBaseUrl}/img/social_teams.png`,
url: "https://www.microsoft.com/microsoft-teams",
},
"Bark": {
logo: `${StaticBaseUrl}/img/social_bark.png`,
url: "https://apps.apple.com/us/app/bark-customed-notifications/id1403753865",
},
"Pushover": {
logo: `${StaticBaseUrl}/img/social_pushover.png`,
url: "https://pushover.net/",
},
"Pushbullet": {
logo: `${StaticBaseUrl}/img/social_pushbullet.png`,
url: "https://www.pushbullet.com/",
},
"Slack": {
logo: `${StaticBaseUrl}/img/social_slack.png`,
url: "https://slack.com/",
},
"Webpush": {
logo: `${StaticBaseUrl}/img/email_default.png`,
url: "https://developer.mozilla.org/en-US/docs/Web/API/Push_API",
},
"Discord": {
logo: `${StaticBaseUrl}/img/social_discord.png`,
url: "https://discord.com/",
},
"Google Chat": {
logo: `${StaticBaseUrl}/img/social_google_chat.png`,
url: "https://workspace.google.com/intl/en/products/chat/",
},
"Line": {
logo: `${StaticBaseUrl}/img/social_line.png`,
url: "https://line.me/",
},
"Matrix": {
logo: `${StaticBaseUrl}/img/social_matrix.png`,
url: "https://www.matrix.org/",
},
"Twitter": {
logo: `${StaticBaseUrl}/img/social_twitter.png`,
url: "https://twitter.com/",
},
"Reddit": {
logo: `${StaticBaseUrl}/img/social_reddit.png`,
url: "https://www.reddit.com/",
},
"Rocket Chat": {
logo: `${StaticBaseUrl}/img/social_rocket_chat.png`,
url: "https://rocket.chat/",
},
"Viber": {
logo: `${StaticBaseUrl}/img/social_viber.png`,
url: "https://www.viber.com/",
},
}, },
}; };
@@ -908,6 +976,7 @@ export function getProviderTypeOptions(category) {
{id: "Default", name: "Default"}, {id: "Default", name: "Default"},
{id: "SUBMAIL", name: "SUBMAIL"}, {id: "SUBMAIL", name: "SUBMAIL"},
{id: "Mailtrap", name: "Mailtrap"}, {id: "Mailtrap", name: "Mailtrap"},
{id: "Azure ACS", name: "Azure ACS"},
] ]
); );
} else if (category === "SMS") { } else if (category === "SMS") {
@@ -973,6 +1042,22 @@ export function getProviderTypeOptions(category) {
return ([ return ([
{id: "Telegram", name: "Telegram"}, {id: "Telegram", name: "Telegram"},
{id: "Custom HTTP", name: "Custom HTTP"}, {id: "Custom HTTP", name: "Custom HTTP"},
{id: "DingTalk", name: "DingTalk"},
{id: "Lark", name: "Lark"},
{id: "Microsoft Teams", name: "Microsoft Teams"},
{id: "Bark", name: "Bark"},
{id: "Pushover", name: "Pushover"},
{id: "Pushbullet", name: "Pushbullet"},
{id: "Slack", name: "Slack"},
{id: "Webpush", name: "Webpush"},
{id: "Discord", name: "Discord"},
{id: "Google Chat", name: "Google Chat"},
{id: "Line", name: "Line"},
{id: "Matrix", name: "Matrix"},
{id: "Twitter", name: "Twitter"},
{id: "Reddit", name: "Reddit"},
{id: "Rocket Chat", name: "Rocket Chat"},
{id: "Viber", name: "Viber"},
]); ]);
} else { } else {
return []; return [];

View File

@@ -178,7 +178,7 @@ class SystemInfo extends React.Component {
<Col span={6}></Col> <Col span={6}></Col>
</Row> </Row>
<Tour <Tour
open={this.state.isTourVisible} open={Setting.isMobile() ? false : this.state.isTourVisible}
onClose={this.setIsTourVisible} onClose={this.setIsTourVisible}
steps={this.getSteps()} steps={this.getSteps()}
indicatorsRender={(current, total) => ( indicatorsRender={(current, total) => (

View File

@@ -182,6 +182,8 @@ class SignupPage extends React.Component {
AuthBackend.signup(values) AuthBackend.signup(values)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
// the user's id will be returned by `signup()`, if user signup by phone, the `username` in `values` is undefined.
values.username = res.data.split("/")[1];
if (Setting.hasPromptPage(application) && (!values.plan || !values.pricing)) { if (Setting.hasPromptPage(application) && (!values.plan || !values.pricing)) {
AuthBackend.getAccount("") AuthBackend.getAccount("")
.then((res) => { .then((res) => {

View File

@@ -303,7 +303,7 @@ export function initWeb3Onboard(application, provider) {
description: "Connect a wallet using Casdoor", description: "Connect a wallet using Casdoor",
recommendedInjectedWallets: [ recommendedInjectedWallets: [
{name: "MetaMask", url: "https://metamask.io"}, {name: "MetaMask", url: "https://metamask.io"},
{name: "Coinbase", url: "https://wallet.coinbase.com/"}, {name: "Coinbase", url: "https://www.coinbase.com/wallet"},
], ],
}; };

View File

@@ -50,11 +50,15 @@ window.fetch = async(url, option = {}) => {
requestFilters.forEach(filter => filter(url, option)); requestFilters.forEach(filter => filter(url, option));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
originalFetch(url, option).then(res => { originalFetch(url, option)
if (!url.startsWith("/api/get-organizations")) { .then(res => {
responseFilters.forEach(filter => filter(res.clone())); if (!url.startsWith("/api/get-organizations")) {
} responseFilters.forEach(filter => filter(res.clone()));
resolve(res); }
}); resolve(res);
})
.catch(error => {
reject(error);
});
}); });
}; };

View File

@@ -105,8 +105,8 @@ const Dashboard = (props) => {
i18next.t("general:Applications"), i18next.t("general:Applications"),
i18next.t("general:Organizations"), i18next.t("general:Organizations"),
i18next.t("general:Subscriptions"), i18next.t("general:Subscriptions"),
]}, ], top: "10%"},
grid: {left: "3%", right: "4%", bottom: "3%", containLabel: true}, grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
xAxis: {type: "category", boundaryGap: false, data: dateArray}, xAxis: {type: "category", boundaryGap: false, data: dateArray},
yAxis: {type: "value"}, yAxis: {type: "value"},
series: [ series: [
@@ -120,24 +120,24 @@ const Dashboard = (props) => {
myChart.setOption(option); myChart.setOption(option);
return ( return (
<Row id="statistic" gutter={80}> <Row id="statistic" gutter={80} justify={"center"}>
<Col span={50}> <Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}> <Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Statistic title={i18next.t("home:Total users")} fontSize="100px" value={dashboardData.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} /> <Statistic title={i18next.t("home:Total users")} fontSize="100px" value={dashboardData.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} />
</Card> </Card>
</Col> </Col>
<Col span={50}> <Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}> <Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Statistic title={i18next.t("home:New users today")} fontSize="100px" value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 1]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} /> <Statistic title={i18next.t("home:New users today")} fontSize="100px" value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 1]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card> </Card>
</Col> </Col>
<Col span={50}> <Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}> <Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Statistic title={i18next.t("home:New users past 7 days")} value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 7]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} /> <Statistic title={i18next.t("home:New users past 7 days")} value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 7]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card> </Card>
</Col> </Col>
<Col span={50}> <Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}> <Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Statistic title={i18next.t("home:New users past 30 days")} value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 30]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} /> <Statistic title={i18next.t("home:New users past 30 days")} value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 30]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card> </Card>
</Col> </Col>
@@ -150,7 +150,7 @@ const Dashboard = (props) => {
{renderEChart()} {renderEChart()}
<div id="echarts-chart" style={{width: "80%", height: "400px", textAlign: "center", marginTop: "20px"}} /> <div id="echarts-chart" style={{width: "80%", height: "400px", textAlign: "center", marginTop: "20px"}} />
<Tour <Tour
open={isTourVisible} open={Setting.isMobile() ? false : isTourVisible}
onClose={setIsTourToLocal} onClose={setIsTourToLocal}
steps={getSteps()} steps={getSteps()}
indicatorsRender={(current, total) => ( indicatorsRender={(current, total) => (

View File

@@ -49,7 +49,11 @@ export const AgreementModal = (props) => {
function getTermsOfUseContent(url) { function getTermsOfUseContent(url) {
return fetch(url, { return fetch(url, {
method: "GET", method: "GET",
}).then(r => r.text()); })
.then(r => r.text())
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to get TermsOfUse URL")}: ${url}, ${error}`);
});
} }
export function isAgreementRequired(application) { export function isAgreementRequired(application) {