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/version_info.txt ./go/src/casdoor/version_info.txt
COPY --from=FRONT /web/build ./web/build
RUN mkdir tempFiles
ENTRYPOINT ["/bin/bash"]
CMD ["/docker-entrypoint.sh"]

View File

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

View File

@@ -90,14 +90,24 @@ func (c *ApiController) GetApplication() {
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))
if err != nil {
c.ResponseError(err.Error())
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))

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) {
userId := user.GetId()
allowed, err := object.CheckAccessPermission(userId, application)
allowed, err := object.CheckLoginPermission(userId, application)
if err != nil {
c.ResponseError(err.Error(), nil)
return

View File

@@ -176,11 +176,10 @@ func (c *ApiController) DeletePayment() {
func (c *ApiController) NotifyPayment() {
owner := c.Ctx.Input.Param(":owner")
paymentName := c.Ctx.Input.Param(":payment")
orderId := c.Ctx.Input.Param("order")
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 {
c.ResponseError(err.Error())
return

View File

@@ -187,11 +187,11 @@ func (c *ApiController) BuyProduct() {
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 {
c.ResponseError(err.Error())
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 400 {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() {
grantType := c.Input().Get("grant_type")
refreshToken := c.Input().Get("refresh_token")

View File

@@ -457,7 +457,16 @@ func (c *ApiController) SetPassword() {
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())
if 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/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b
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.44.4
github.com/aws/aws-sdk-go v1.45.5
github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0
github.com/casbin/casbin v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.30.1
github.com/casdoor/go-sms-sender v0.12.0
github.com/casbin/casbin/v2 v2.37.0
github.com/casdoor/go-sms-sender v0.14.0
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/xorm-adapter/v3 v3.0.4
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-webauthn/webauthn v0.6.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/lestrrat-go/jwx v1.2.21
github.com/lib/pq v1.10.9
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
github.com/markbates/goth v1.75.2
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/nyaruka/phonenumbers v1.1.5
github.com/pquerna/otp v1.4.0
@@ -60,10 +59,12 @@ require (
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.11.0
golang.org/x/net v0.13.0
golang.org/x/oauth2 v0.10.0
golang.org/x/crypto v0.12.0
golang.org/x/net v0.14.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/square/go-jose.v2 v2.6.0
maunium.net/go/mautrix v0.16.0
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
// 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 {
UserId string `json:"user_id"`
}
// 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) {
params := url.Values{}
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
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" {
return NewTelegramProvider(appId, receiver)
} else if typ == "Custom HTTP" {
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

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"
"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"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/telegram"
)
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 {
return nil, err
}
t := &telegram.Telegram{}
t.SetClient(client)
telegramSrv := &telegram.Telegram{}
telegramSrv.SetClient(client)
chatId, err := strconv.ParseInt(chatIdStr, 10, 64)
if err != nil {
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 {
isValid := false
for _, targetUri := range application.RedirectUris {
redirectUris := append([]string{"http://localhost:"}, application.RedirectUris...)
for _, targetUri := range redirectUris {
targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
isValid = true
break
return true
}
}
return isValid
return false
}
func IsOriginAllowed(origin string) (bool, error) {

View File

@@ -33,10 +33,8 @@ type Cert struct {
BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"`
Certificate string `xorm:"mediumtext" json:"certificate"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"`
AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"`
Certificate string `xorm:"mediumtext" json:"certificate"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
}
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"))
}
func CheckAccessPermission(userId string, application *Application) (bool, error) {
func CheckLoginPermission(userId string, application *Application) (bool, error) {
var err error
if userId == "built-in/admin" {
return true, nil
@@ -361,32 +361,40 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
return false, err
}
allowed := true
allowCount := 0
denyCount := 0
for _, permission := range permissions {
if !permission.IsEnabled {
if !permission.IsEnabled || permission.ResourceType != "Application" || !permission.isResourceHit(application.Name) {
continue
}
isHit := false
for _, resource := range permission.Resources {
if application.Name == resource {
isHit = true
break
}
if permission.isUserHit(userId) {
allowCount += 1
}
if isHit {
containsAsterisk := ContainsAsterisk(userId, permission.Users)
if containsAsterisk {
return true, err
enforcer := getPermissionEnforcer(permission)
var isAllowed bool
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)
if allowed, err = enforcer.Enforce(userId, application.Name, "read"); allowed {
return allowed, err
} else {
if permission.Effect == "Deny" {
denyCount += 1
}
}
}
return allowed, err
if denyCount > 0 {
return false, nil
}
return true, nil
}
func CheckUsername(username string, lang string) string {

View File

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

View File

@@ -19,7 +19,6 @@ import (
"fmt"
"time"
"github.com/beego/beego"
"github.com/beego/beego/context"
"github.com/google/uuid"
"github.com/pquerna/otp"
@@ -39,10 +38,11 @@ type TotpMfa struct {
}
func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
issuer := beego.AppConfig.String("appname")
if issuer == "" {
issuer = "casdoor"
}
//issuer := beego.AppConfig.String("appname")
//if issuer == "" {
// issuer = "casdoor"
//}
issuer := "casdoor"
key, err := totp.Generate(totp.GenerateOpts{
Issuer: issuer,
@@ -81,12 +81,15 @@ func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
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,
Skew: 1,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
if err != nil {
return err
}
if result {
return nil
@@ -125,7 +128,15 @@ func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) 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 {
return nil

View File

@@ -18,12 +18,12 @@ import (
"context"
"github.com/casdoor/casdoor/notification"
"github.com/nikoksr/notify"
"github.com/casdoor/notify"
)
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
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 {
return nil, err
}

View File

@@ -16,7 +16,6 @@ package object
import (
"fmt"
"net/http"
"github.com/casdoor/casdoor/pp"
@@ -55,6 +54,7 @@ type Payment struct {
// Order Info
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
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"`
Message string `xorm:"varchar(2000)" json:"message"`
}
@@ -152,7 +152,7 @@ func DeletePayment(payment *Payment) (bool, error) {
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)
if err != nil {
return nil, nil, err
@@ -166,7 +166,7 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
if err != nil {
return nil, nil, err
}
pProvider, cert, err := provider.getPaymentProvider()
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return nil, nil, err
}
@@ -180,11 +180,7 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
return nil, nil, err
}
if orderId == "" {
orderId = payment.OutOrderId
}
notifyResult, err := pProvider.Notify(request, body, cert.AuthorityPublicKey, orderId)
notifyResult, err := pProvider.Notify(body, payment.OutOrderId)
if err != nil {
return payment, nil, err
}
@@ -205,8 +201,8 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
return payment, notifyResult, nil
}
func NotifyPayment(request *http.Request, body []byte, owner string, paymentName string, orderId string) (*Payment, error) {
payment, notifyResult, err := notifyPayment(request, body, owner, paymentName, orderId)
func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, error) {
payment, notifyResult, err := notifyPayment(body, owner, paymentName)
if payment != nil {
if err != nil {
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)
}
pProvider, _, err := provider.getPaymentProvider()
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return "", err
}

View File

@@ -30,6 +30,7 @@ type Permission struct {
Description string `xorm:"varchar(100)" json:"description"`
Users []string `xorm:"mediumtext" json:"users"`
Groups []string `xorm:"mediumtext" json:"groups"`
Roles []string `xorm:"mediumtext" json:"roles"`
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
func (p *Permission) GetId() string {
return util.GetId(p.Owner, p.Name)
}
func GetPermissionCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Permission{})
@@ -345,20 +342,6 @@ func GetPermissionsByModel(owner string, model string) ([]*Permission, error) {
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 {
for _, permission := range permissions {
permission.Users = nil
@@ -388,3 +371,27 @@ func GroupPermissionsByModelAdapter(permissions []*Permission) map[string][]stri
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"`
Quantity int `json:"quantity"`
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"`
State string `xorm:"varchar(100)" json:"state"`
@@ -158,24 +158,23 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
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)
if err != nil {
return "", "", err
return nil, err
}
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)
if err != nil {
return "", "", err
return nil, err
}
pProvider, _, err := provider.getPaymentProvider()
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return "", "", err
return nil, err
}
owner := product.Owner
@@ -192,15 +191,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if pricingName != "" && planName != "" {
plan, err := GetPlan(util.GetId(owner, planName))
if err != nil {
return "", "", err
return nil, err
}
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)
_, err = AddSubscription(sub)
if err != nil {
return "", "", err
return nil, err
}
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
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil {
return "", "", err
return nil, err
}
// Create a Payment linked with Product and Order
payment := Payment{
payment := &Payment{
Owner: product.Owner,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
@@ -230,6 +229,7 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
User: user.Name,
PayUrl: payUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated,
OutOrderId: orderId,
}
@@ -238,15 +238,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
payment.State = pp.PaymentStatePaid
}
affected, err := AddPayment(&payment)
affected, err := AddPayment(payment)
if err != nil {
return "", "", err
return nil, err
}
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 {

View File

@@ -17,31 +17,24 @@
package object
import (
"testing"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
)
func TestProduct(t *testing.T) {
InitConfig()
product, _ := GetProduct("admin/product_123")
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)
if err != nil {
panic(err)
}
paymentName := util.GenerateTimeId()
returnUrl := ""
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)
}
//func TestProduct(t *testing.T) {
// InitConfig()
//
// product, _ := GetProduct("admin/product_123")
// 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)
// if err != nil {
// panic(err)
// }
//
// paymentName := util.GenerateTimeId()
// returnUrl := ""
// 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
}
func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
cert := &Cert{}
if p.Cert != "" {
var err error
cert, err = getCert(p.Owner, p.Cert)
cert, err = GetCert(util.GetId(p.Owner, p.Cert))
if err != nil {
return nil, nil, err
return nil, err
}
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)
}
}
pProvider, err := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, p.ClientId2)
if err != nil {
return nil, cert, err
typ := p.Type
if typ == "Dummy" {
pp, err := pp.NewDummyPaymentProvider()
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, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
}
return pProvider, cert, nil
return nil, nil
}
func (p *Provider) GetId() string {

View File

@@ -15,6 +15,7 @@
package object
import (
"database/sql"
"fmt"
"strings"
"time"
@@ -31,7 +32,7 @@ type Credential struct {
}
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)
if err != nil {
return nil, err

View File

@@ -15,6 +15,7 @@
package object
import (
"database/sql"
"encoding/json"
"fmt"
"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{}
for _, result := range results {
originalUser := &OriginalUser{
@@ -216,11 +217,11 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
names := strings.Split(tableColumnName, "+")
var values []string
for _, name := range names {
values = append(values, result[strings.Trim(name, " ")])
values = append(values, result[strings.Trim(name, " ")].String)
}
value = strings.Join(values, " ")
} else {
value = result[tableColumnName]
value = result[tableColumnName].String
}
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value)
}
@@ -249,9 +250,9 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
// enable
value, ok := result["ENABLED"]
if ok {
originalUser.IsForbidden = !util.ParseBool(value)
originalUser.IsForbidden = !util.ParseBool(value.String)
} 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) {
if user, err := GetUser(userId); err != nil {
user, err := GetUser(userId)
if err != nil {
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

View File

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

View File

@@ -16,9 +16,9 @@ package pp
import (
"context"
"net/http"
"encoding/json"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"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) {
// clientId => appId
// cert.Certificate => appCertificate
// cert.PrivateKey => appPrivateKey
// rootCert.Certificate => authorityPublicKey
// rootCert.PrivateKey => authorityRootPublicKey
pp := &AlipayPaymentProvider{}
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) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
bm.Set("providerName", providerName)
bm.Set("productName", productName)
bm.Set("return_url", returnUrl)
bm.Set("notify_url", notifyUrl)
bm.Set("subject", productDisplayName)
pp.Client.SetReturnUrl(returnUrl)
pp.Client.SetNotifyUrl(notifyUrl)
bm.Set("subject", joinAttachString([]string{productName, productDisplayName, providerName}))
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)
if err != nil {
return "", "", err
}
return payUrl, "", nil
return payUrl, paymentName, nil
}
func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
bm, err := alipay.ParseNotifyToBodyMap(request)
func (pp *AlipayPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
bm := gopay.BodyMap{}
bm.Set("out_trade_no", orderId)
aliRsp, err := pp.Client.TradeQuery(context.Background(), bm)
notifyResult := &NotifyResult{}
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
}
providerName := bm.Get("providerName")
productName := bm.Get("productName")
productDisplayName := bm.Get("subject")
paymentName := bm.Get("out_trade_no")
price := util.ParseFloat(bm.Get("total_amount"))
ok, err := alipay.VerifySignWithCert(authorityPublicKey, bm)
if err != nil {
return nil, err
switch aliRsp.Response.TradeStatus {
case "WAIT_BUYER_PAY":
notifyResult.PaymentStatus = PaymentStateCreated
return notifyResult, nil
case "TRADE_CLOSED":
notifyResult.PaymentStatus = PaymentStateTimeout
return notifyResult, nil
case "TRADE_SUCCESS":
// skip
default:
notifyResult.PaymentStatus = PaymentStateError
notifyResult.NotifyMessage = fmt.Sprintf("unexpected alipay trade state: %v", aliRsp.Response.TradeStatus)
return notifyResult, nil
}
if !ok {
return nil, err
}
notifyResult := &NotifyResult{
productDisplayName, productName, providerName, _ := parseAttachString(aliRsp.Response.Subject)
notifyResult = &NotifyResult{
ProductName: productName,
ProductDisplayName: productDisplayName,
ProviderName: providerName,
OrderId: orderId,
PaymentStatus: PaymentStatePaid,
Price: price,
PaymentName: paymentName,
Price: priceStringToFloat64(aliRsp.Response.TotalAmount),
PaymentName: orderId,
}
return notifyResult, nil
}

View File

@@ -14,8 +14,6 @@
package pp
import "net/http"
type DummyPaymentProvider struct{}
func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
@@ -27,7 +25,7 @@ func (pp *DummyPaymentProvider) Pay(providerName string, productName string, pay
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{
PaymentStatus: PaymentStatePaid,
}, nil

View File

@@ -216,7 +216,7 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
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{}
m, err := url.ParseQuery(string(body))
if err != nil {

View File

@@ -18,7 +18,6 @@ import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"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
}
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{}
captureRsp, err := pp.Client.OrderCapture(context.Background(), orderId, nil)
if err != nil {

View File

@@ -14,8 +14,6 @@
package pp
import "net/http"
type PaymentState string
const (
@@ -42,45 +40,7 @@ type NotifyResult struct {
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)
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)
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 (
"fmt"
"net/http"
"time"
"github.com/casdoor/casdoor/conf"
@@ -94,7 +93,7 @@ func (pp *StripePaymentProvider) Pay(providerName string, productName string, pa
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{}
sCheckout, err := stripeCheckout.Get(orderId, nil)
if err != nil {

View File

@@ -49,3 +49,11 @@ func priceFloat64ToInt64(price float64) int64 {
func priceFloat64ToString(price float64) string {
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 (
"context"
"errors"
"net/http"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
@@ -31,17 +31,22 @@ type WechatPayNotifyResponse struct {
type WechatPaymentProvider struct {
Client *wechat.ClientV3
appId string
AppId string
}
func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, mchCertSerialNumber string, privateKey string) (*WechatPaymentProvider, error) {
if appId == "" && mchId == "" && mchCertSerialNumber == "" && apiV3Key == "" && privateKey == "" {
func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, serialNo string, privateKey string) (*WechatPaymentProvider, error) {
// 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
}
pp := &WechatPaymentProvider{appId: appId}
clientV3, err := wechat.NewClientV3(mchId, mchCertSerialNumber, apiV3Key, privateKey)
clientV3, err := wechat.NewClientV3(mchId, serialNo, apiV3Key, privateKey)
if err != nil {
return nil, err
}
@@ -50,73 +55,70 @@ func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, mchCe
if err != nil {
return nil, err
}
pp.Client = clientV3.SetPlatformCert([]byte(platformCert), serialNo)
pp := &WechatPaymentProvider{
Client: clientV3.SetPlatformCert([]byte(platformCert), serialNo),
AppId: appId,
}
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) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName}))
bm.Set("appid", pp.appId)
bm.Set("appid", pp.AppId)
bm.Set("description", productDisplayName)
bm.Set("notify_url", notifyUrl)
bm.Set("out_trade_no", paymentName)
bm.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", int(price*100))
bm.Set("currency", "CNY")
bm.Set("total", priceFloat64ToInt64(price))
bm.Set("currency", currency)
})
wxRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return "", "", err
}
if wxRsp.Code != wechat.Success {
return "", "", errors.New(wxRsp.Error)
if nativeRsp.Code != wechat.Success {
return "", "", errors.New(nativeRsp.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) {
notifyReq, err := wechat.V3ParseNotify(request)
func (pp *WechatPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
notifyResult := &NotifyResult{}
queryRsp, err := pp.Client.V3TransactionQueryOrder(context.Background(), wechat.OutTradeNo, orderId)
if err != nil {
return nil, err
}
cert := pp.Client.WxPublicKey()
err = notifyReq.VerifySignByPK(cert)
if err != nil {
return nil, err
if queryRsp.Code != wechat.Success {
return nil, errors.New(queryRsp.Error)
}
apiKey := string(pp.Client.ApiV3Key)
result, err := notifyReq.DecryptCipherText(apiKey)
if err != nil {
return nil, err
switch queryRsp.Response.TradeState {
case "SUCCESS":
// skip
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
}
paymentName := result.OutTradeNo
price := float64(result.Amount.PayerTotal) / 100
productDisplayName, productName, providerName, err := parseAttachString(result.Attach)
if err != nil {
return nil, err
}
notifyResult := &NotifyResult{
productDisplayName, productName, providerName, _ := parseAttachString(queryRsp.Response.Attach)
notifyResult = &NotifyResult{
ProductName: productName,
ProductDisplayName: productDisplayName,
ProviderName: providerName,
OrderId: orderId,
Price: price,
Price: priceInt64ToFloat64(int64(queryRsp.Response.Amount.Total)),
PaymentStatus: PaymentStatePaid,
PaymentName: paymentName,
PaymentName: queryRsp.Response.OutTradeNo,
}
return notifyResult, nil
}

View File

@@ -29,7 +29,13 @@ func AutoSigninFilter(ctx *context.Context) {
// GET parameter like "/page?access_token=123" or
// 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 != "" {
token, err := object.GetTokenByAccessToken(accessToken)

View File

@@ -40,6 +40,13 @@ func CorsFilter(ctx *context.Context) {
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 {
ok, err := object.IsOriginAllowed(origin)
if err != nil {

View File

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

View File

@@ -15,6 +15,7 @@
package storage
import (
"fmt"
"io"
"os"
"path/filepath"
@@ -23,76 +24,69 @@ import (
"github.com/casdoor/oss"
)
var baseFolder = "files"
// FileSystem file system storage
type FileSystem struct {
Base string
// LocalFileSystemProvider file system storage
type LocalFileSystemProvider struct {
BaseDir string
}
// NewFileSystem initialize the local file system storage
func NewFileSystem(base string) *FileSystem {
absBase, err := filepath.Abs(base)
// NewLocalFileSystemStorageProvider initialize the local file system storage
func NewLocalFileSystemStorageProvider() *LocalFileSystemProvider {
baseFolder := "files"
absBase, err := filepath.Abs(baseFolder)
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
func (fileSystem FileSystem) GetFullPath(path string) string {
func (sp LocalFileSystemProvider) GetFullPath(path string) string {
fullPath := path
if !strings.HasPrefix(path, fileSystem.Base) {
fullPath, _ = filepath.Abs(filepath.Join(fileSystem.Base, path))
if !strings.HasPrefix(path, sp.BaseDir) {
fullPath, _ = filepath.Abs(filepath.Join(sp.BaseDir, path))
}
return fullPath
}
// Get receive file with given path
func (fileSystem FileSystem) Get(path string) (*os.File, error) {
return os.Open(fileSystem.GetFullPath(path))
func (sp LocalFileSystemProvider) Get(path string) (*os.File, error) {
return os.Open(sp.GetFullPath(path))
}
// GetStream get file as stream
func (fileSystem FileSystem) GetStream(path string) (io.ReadCloser, error) {
return os.Open(fileSystem.GetFullPath(path))
func (sp LocalFileSystemProvider) GetStream(path string) (io.ReadCloser, error) {
return os.Open(sp.GetFullPath(path))
}
// Put store a reader into given path
func (fileSystem FileSystem) Put(path string, reader io.Reader) (*oss.Object, error) {
var (
fullPath = fileSystem.GetFullPath(path)
err = os.MkdirAll(filepath.Dir(fullPath), os.ModePerm)
)
func (sp LocalFileSystemProvider) Put(path string, reader io.Reader) (*oss.Object, error) {
fullPath := sp.GetFullPath(path)
err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm)
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))
if err == nil {
if seeker, ok := reader.(io.ReadSeeker); ok {
seeker.Seek(0, 0)
}
_, err = io.Copy(dst, reader)
}
return &oss.Object{Path: path, Name: filepath.Base(path), StorageInterface: fileSystem}, err
return &oss.Object{Path: path, Name: filepath.Base(path), StorageInterface: sp}, err
}
// Delete delete file
func (fileSystem FileSystem) Delete(path string) error {
return os.Remove(fileSystem.GetFullPath(path))
func (sp LocalFileSystemProvider) Delete(path string) error {
return os.Remove(sp.GetFullPath(path))
}
// List list all objects under current path
func (fileSystem FileSystem) List(path string) ([]*oss.Object, error) {
var (
objects []*oss.Object
fullPath = fileSystem.GetFullPath(path)
)
func (sp LocalFileSystemProvider) List(path string) ([]*oss.Object, error) {
objects := []*oss.Object{}
fullPath := sp.GetFullPath(path)
filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
if path == fullPath {
@@ -102,10 +96,10 @@ func (fileSystem FileSystem) List(path string) ([]*oss.Object, error) {
if err == nil && !info.IsDir() {
modTime := info.ModTime()
objects = append(objects, &oss.Object{
Path: strings.TrimPrefix(path, fileSystem.Base),
Path: strings.TrimPrefix(path, sp.BaseDir),
Name: info.Name(),
LastModified: &modTime,
StorageInterface: fileSystem,
StorageInterface: sp,
})
}
return nil
@@ -114,16 +108,12 @@ func (fileSystem FileSystem) List(path string) ([]*oss.Object, error) {
return objects, nil
}
// GetEndpoint get endpoint, FileSystem's endpoint is /
func (fileSystem FileSystem) GetEndpoint() string {
// GetEndpoint get endpoint, LocalFileSystemProvider's endpoint is /
func (sp LocalFileSystemProvider) GetEndpoint() string {
return "/"
}
// 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
}
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 {
switch providerType {
case "Local File System":
return NewLocalFileSystemStorageProvider(clientId, clientSecret, region, bucket, endpoint)
return NewLocalFileSystemStorageProvider()
case "AWS S3":
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "MinIO":

View File

@@ -187,32 +187,6 @@ func IsStringsEmpty(strs ...string) bool {
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 {
data, err := os.ReadFile(filepath.Clean(path))
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) {
scenarios := []struct {
description string

View File

@@ -34,6 +34,7 @@
"i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19",
"moment": "^2.29.1",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1",
@@ -82,6 +83,9 @@
"@babel/eslint-parser": "^7.18.9",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@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",
"cypress": "^12.5.1",
"eslint": "8.22.0",
@@ -91,10 +95,7 @@
"lint-staged": "^13.0.3",
"stylelint": "^14.11.0",
"stylelint-config-recommended-less": "^1.0.4",
"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"
"stylelint-config-standard": "^28.0.0"
},
"lint-staged": {
"src/**/*.{css,less}": [

View File

@@ -664,7 +664,8 @@ class App extends Component {
window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/auto-signup") ||
window.location.pathname.startsWith("/select-plan") ||
window.location.pathname.startsWith("/buy-plan");
window.location.pathname.startsWith("/buy-plan") ||
window.location.pathname.startsWith("/qrcode") ;
}
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"))} :
</Col>
<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);
}} />
<Upload maxCount={1} accept=".html" showUploadList={false}

View File

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

View File

@@ -31,6 +31,7 @@ import CasLogout from "./auth/CasLogout";
import {authConfig} from "./auth/Auth";
import ProductBuyPage from "./ProductBuyPage";
import PaymentResultPage from "./PaymentResultPage";
import QrCodePage from "./QrCodePage";
class EntryPage extends React.Component {
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="/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="/qrcode/:owner/:paymentName" render={(props) => <QrCodePage {...this.props} onUpdateApplication={onUpdateApplication} {...props} />} />
</Switch>
</div>
);

View File

@@ -101,7 +101,7 @@ class PaymentResultPage extends React.Component {
payment: payment,
});
if (payment.state === "Created") {
if (["PayPal", "Stripe"].includes(payment.type)) {
if (["PayPal", "Stripe", "Alipay"].includes(payment.type)) {
this.setState({
timeout: setTimeout(async() => {
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 OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as GroupBackend from "./backend/GroupBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend";
@@ -35,6 +36,7 @@ class PermissionEditPage extends React.Component {
organizations: [],
model: null,
users: [],
groups: [],
roles: [],
models: [],
resources: [],
@@ -67,6 +69,7 @@ class PermissionEditPage extends React.Component {
});
this.getUsers(permission.owner);
this.getGroups(permission.owner);
this.getRoles(permission.owner);
this.getModels(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) {
RoleBackend.getRoles(organizationName)
.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 => {
this.updatePermissionField("owner", owner);
this.getUsers(owner);
this.getGroups(owner);
this.getRoles(owner);
this.getModels(owner);
this.getResources(owner);
@@ -263,6 +281,17 @@ class PermissionEditPage extends React.Component {
/>
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{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(),
displayName: `New Permission - ${randomName}`,
users: [`${this.props.account.owner}/${this.props.account.name}`],
groups: [],
roles: [],
domains: [],
resourceType: "Application",
@@ -179,6 +180,17 @@ class PermissionListPage extends BaseListPage {
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"),
dataIndex: "roles",

View File

@@ -13,8 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Descriptions, Modal, Spin} from "antd";
import {CheckCircleTwoTone} from "@ant-design/icons";
import {Button, Descriptions, Spin} from "antd";
import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend";
import * as PlanBackend from "./backend/PlanBackend";
@@ -36,7 +35,6 @@ class ProductBuyPage extends React.Component {
pricing: props?.pricing ?? null,
plan: null,
isPlacingOrder: false,
qrCodeModalProvider: null,
};
}
@@ -130,13 +128,6 @@ class ProductBuyPage extends React.Component {
}
buyProduct(product, provider) {
if (provider.clientId.startsWith("http")) {
this.setState({
qrCodeModalProvider: provider,
});
return;
}
this.setState({
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 ?? "")
.then((res) => {
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);
} else {
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) {
let text = provider.type;
if (provider.type === "Dummy") {
@@ -290,9 +246,6 @@ class ProductBuyPage extends React.Component {
</Descriptions.Item>
</Descriptions>
</Spin>
{
this.renderQrCodeModal()
}
</div>
);
}

View File

@@ -16,6 +16,8 @@ import React from "react";
import {Button, Card, Checkbox, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as ProviderBackend from "./backend/ProviderBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {authConfig} from "./auth/Auth";
@@ -24,7 +26,6 @@ import * as ProviderNotification from "./common/TestNotificationWidget";
import * as ProviderEditTestSms from "./common/TestSmsWidget";
import copy from "copy-to-clipboard";
import {CaptchaPreview} from "./common/CaptchaPreview";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import * as Web3Auth from "./auth/Web3Auth";
@@ -39,6 +40,7 @@ class ProviderEditPage extends React.Component {
providerName: props.match.params.providerName,
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
provider: null,
certs: [],
organizations: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
@@ -47,6 +49,7 @@ class ProviderEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getOrganizations();
this.getProvider();
this.getCerts(this.state.owner);
}
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) {
if (["port"].includes(key)) {
value = Setting.myParseInt(value);
@@ -91,6 +105,11 @@ class ProviderEditPage extends React.Component {
value = this.parseProviderField(key, value);
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;
this.setState({
provider: provider,
@@ -182,6 +201,12 @@ class ProviderEditPage extends React.Component {
} else {
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:
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");
}
} else if (provider.category === "Email") {
if (provider.type === "SUBMAIL") {
if (provider.type === "SUBMAIL" || provider.type === "Azure ACS") {
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
}
} 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");
tooltip = i18next.t("provider:App Key - Tooltip");
}
@@ -305,16 +333,25 @@ class ProviderEditPage extends React.Component {
let text = "";
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");
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");
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 === "") {
return null;
return (
<React.Fragment>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Test Notification", "Test Notification")} :
</Col>
</React.Fragment>
);
} else {
return (
<React.Fragment>
@@ -406,7 +443,6 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Twilio SMS");
} else if (value === "Storage") {
this.updateProviderField("type", "AWS S3");
this.updateProviderField("domain", Setting.getFullServerUrl());
} else if (value === "SAML") {
this.updateProviderField("type", "Keycloak");
} 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 === "Email" && this.state.provider.type === "Azure ACS") ||
(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>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel(this.state.provider)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField("clientId", e.target.value);
}} />
</Col>
</Row>
{
this.state.provider.type === "Line" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel(this.state.provider)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField("clientId", e.target.value);
}} />
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{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>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@@ -630,7 +672,7 @@ class ProviderEditPage extends React.Component {
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecret2Label(this.state.provider)} :
@@ -783,6 +825,18 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
) : 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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Content"), i18next.t("provider:Content - Tooltip"))} :
@@ -813,26 +867,30 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.provider.port} onChange={value => {
this.updateProviderField("port", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
this.updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
{["Azure ACS"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.provider.port} onChange={value => {
this.updateProviderField("port", value);
}} />
</Col>
</Row>
)}
{["Azure ACS"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
this.updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{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);
}} />
</Col>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test SMTP Connection")}
</Button>
{["Azure ACS"].includes(this.state.provider.type) ? null : (
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test SMTP Connection")}
</Button>
)}
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidEmail(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} >
<Input.Group compact>
<CountryCodeSelect
style={{width: "30%"}}
value={this.state.provider.content}
style={{width: "90px"}}
initValue={this.state.provider.content}
onChange={(value) => {
this.updateProviderField("content", value);
}}
countryCodes={this.props.account.organization.countryCodes}
/>
<Input value={this.state.provider.receiver}
style={{width: "70%"}}
style={{width: "150px"}}
placeholder = {i18next.t("user:Input your phone number")}
onChange={e => {
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"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.cert} onChange={e => {
this.updateProviderField("cert", e.target.value);
}} />
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.cert} onChange={(value => {this.updateProviderField("cert", 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>
</Row>
) : 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`,
url: "https://mailtrap.io",
},
"Azure ACS": {
logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://learn.microsoft.com/zh-cn/azure/communication-services",
},
},
Storage: {
"Local File System": {
@@ -283,6 +287,70 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/email_default.png`,
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: "SUBMAIL", name: "SUBMAIL"},
{id: "Mailtrap", name: "Mailtrap"},
{id: "Azure ACS", name: "Azure ACS"},
]
);
} else if (category === "SMS") {
@@ -973,6 +1042,22 @@ export function getProviderTypeOptions(category) {
return ([
{id: "Telegram", name: "Telegram"},
{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 {
return [];

View File

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

View File

@@ -182,6 +182,8 @@ class SignupPage extends React.Component {
AuthBackend.signup(values)
.then((res) => {
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)) {
AuthBackend.getAccount("")
.then((res) => {

View File

@@ -303,7 +303,7 @@ export function initWeb3Onboard(application, provider) {
description: "Connect a wallet using Casdoor",
recommendedInjectedWallets: [
{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));
return new Promise((resolve, reject) => {
originalFetch(url, option).then(res => {
if (!url.startsWith("/api/get-organizations")) {
responseFilters.forEach(filter => filter(res.clone()));
}
resolve(res);
});
originalFetch(url, option)
.then(res => {
if (!url.startsWith("/api/get-organizations")) {
responseFilters.forEach(filter => filter(res.clone()));
}
resolve(res);
})
.catch(error => {
reject(error);
});
});
};

View File

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

View File

@@ -49,7 +49,11 @@ export const AgreementModal = (props) => {
function getTermsOfUseContent(url) {
return fetch(url, {
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) {